primocode 8.42.1 → 9.0.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 (102) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +599 -327
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +31 -192
  7. package/lib/catalogo.js +60 -60
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +545 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +606 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +13 -1
  23. package/lib/marca.js +4 -1
  24. package/lib/marketplace.js +189 -0
  25. package/lib/permissao.js +137 -0
  26. package/lib/registro.js +229 -0
  27. package/lib/skills.js +329 -0
  28. package/lib/tools.js +51 -181
  29. package/lib/ui.js +7 -3
  30. package/lib/voz.js +239 -0
  31. package/package.json +12 -3
  32. package/skills/acessibilidade/SKILL.md +28 -0
  33. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  34. package/skills/depurar-de-verdade/SKILL.md +29 -0
  35. package/skills/editar-meu-video/SKILL.md +59 -0
  36. package/skills/escrever-em-portugues/SKILL.md +28 -0
  37. package/skills/git-limpo/SKILL.md +28 -0
  38. package/skills/motion-de-primeira/SKILL.md +62 -0
  39. package/skills/pagina-que-converte/SKILL.md +28 -0
  40. package/skills/performance-web/SKILL.md +28 -0
  41. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  42. package/skills/slides-que-prendem/SKILL.md +40 -0
  43. package/studio/edge.py +16 -66
  44. package/studio/narrar.py +274 -0
  45. package/studio/pessoa.py +167 -0
  46. package/voz/primo_voz/__init__.py +0 -0
  47. package/voz/primo_voz/__main__.py +325 -0
  48. package/voz/primo_voz/escuta.py +235 -0
  49. package/voz/primo_voz/orbe.py +216 -0
  50. package/lib/audio-livre.js +0 -177
  51. package/lib/chaves.js +0 -149
  52. package/lib/claude-engine.js +0 -631
  53. package/lib/codex-engine.js +0 -208
  54. package/lib/remotion.js +0 -663
  55. package/lib/studio.js +0 -960
  56. package/studio/PRIMOCODE.md +0 -58
  57. package/studio/README.md +0 -154
  58. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  59. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  60. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  61. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  62. package/studio/ferramentas/00-protocolo.md +0 -125
  63. package/studio/ferramentas/01-modelos.md +0 -270
  64. package/studio/ferramentas/README.md +0 -32
  65. package/studio/ferramentas/corte.md +0 -919
  66. package/studio/ferramentas/grade.md +0 -92
  67. package/studio/ferramentas/prisma.md +0 -107
  68. package/studio/ferramentas/prosa.md +0 -86
  69. package/studio/ferramentas/tela.md +0 -93
  70. package/studio/ferramentas/traco.md +0 -79
  71. package/studio/server.py +0 -2254
  72. package/studio/studio.log +0 -57
  73. package/studio/voz.py +0 -388
  74. package/studio/web/analise.html +0 -52
  75. package/studio/web/core.css +0 -1167
  76. package/studio/web/fontes/bebas.woff2 +0 -0
  77. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  78. package/studio/web/fontes/inter-400.woff2 +0 -0
  79. package/studio/web/fontes/inter-700.woff2 +0 -0
  80. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  81. package/studio/web/fontes/playfair-700.woff2 +0 -0
  82. package/studio/web/fontes.css +0 -23
  83. package/studio/web/home.html +0 -217
  84. package/studio/web/js/analise.js +0 -510
  85. package/studio/web/js/audio.js +0 -607
  86. package/studio/web/js/editor.js +0 -1802
  87. package/studio/web/js/exportar.js +0 -222
  88. package/studio/web/js/ferramentas.js +0 -108
  89. package/studio/web/js/icones.js +0 -120
  90. package/studio/web/js/midia.js +0 -157
  91. package/studio/web/js/narracao.js +0 -325
  92. package/studio/web/js/player.js +0 -903
  93. package/studio/web/js/render-grade.js +0 -243
  94. package/studio/web/js/render-prosa.js +0 -104
  95. package/studio/web/js/render.js +0 -1852
  96. package/studio/web/js/sfx.js +0 -233
  97. package/studio/web/js/video.js +0 -464
  98. package/studio/web/js/voz-neural.js +0 -152
  99. package/studio/web/render.css +0 -1352
  100. package/studio/web/tf.html +0 -15
  101. package/studio/web/tool.html +0 -78
  102. package/studio/web/view.html +0 -27
package/lib/acervo.js CHANGED
@@ -358,7 +358,14 @@ async function tirarFundo(arquivo, { destino, cor = '0x00FF00', tolerancia = 0.3
358
358
  });
359
359
  if (!metodo) {
360
360
  return { ok: false, error: 'nada nesta máquina sabe recortar fundo',
361
- comoResolver: 'instale com: pip install rembg (ou pipx install rembg)' };
361
+ // Os dois caminhos, porque servem a casos diferentes: o rembg
362
+ // recorta qualquer foto (é modelo de segmentação), o ffmpeg só
363
+ // resolve fundo de cor chapada — mas o ffmpeg já é preciso para
364
+ // o resto do estúdio, então quem tiver um vídeo rodando aqui
365
+ // provavelmente já o tem.
366
+ comoResolver: 'para recortar qualquer imagem: pip install rembg (ou pipx install rembg). '
367
+ + 'Para fundo de cor chapada basta o ffmpeg, que o estúdio já usa: '
368
+ + 'brew install ffmpeg / sudo apt install ffmpeg.' };
362
369
  }
363
370
 
364
371
  const rodar = (cmd, args) => new Promise((ok) => {
package/lib/act.js CHANGED
@@ -18,6 +18,10 @@ const api = require('./api');
18
18
  const cfg = require('./config');
19
19
  const catalogo = require('./catalogo');
20
20
  const repeticao = require('./repeticao');
21
+ const diff = require('./diff');
22
+ const registro = require('./registro');
23
+ const permissao = require('./permissao');
24
+ const skills = require('./skills');
21
25
 
22
26
  // 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
23
27
  // volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
@@ -103,6 +107,29 @@ function ident(profundidade) {
103
107
  return ' '.repeat(profundidade);
104
108
  }
105
109
 
110
+ /* O bloco de diff embaixo do resultado.
111
+ *
112
+ * O `_antes`/`_depois` vem das tools de arquivo e existe só para isto: são as
113
+ * duas versões do texto, que o `limparParaOModelo` apaga antes de a conversa
114
+ * seguir. Sem essa limpeza, cada edição mandaria o arquivo inteiro de volta
115
+ * ao modelo — que acabou de escrevê-lo e não aprende nada relendo. */
116
+ function mostrarDiff(result, profundidade) {
117
+ if (!result || typeof result._antes !== 'string' || typeof result._depois !== 'string') return;
118
+ const bloco = diff.desenhar(result._antes, result._depois, {
119
+ prefixo: fala.recuo(profundidade),
120
+ });
121
+ if (bloco) console.log(bloco);
122
+ }
123
+
124
+ /* O que NÃO pode ir para a conversa. Chave com `_` é desenho de tela: o
125
+ arquivo inteiro, duas vezes, por edição. */
126
+ function limparParaOModelo(result) {
127
+ if (!result || typeof result !== 'object') return result;
128
+ const limpo = {};
129
+ for (const [k, v] of Object.entries(result)) if (!k.startsWith('_')) limpo[k] = v;
130
+ return limpo;
131
+ }
132
+
106
133
  function logResult(kind, result, profundidade) {
107
134
  const p = fala.resultado(profundidade);
108
135
  if (!result) return;
@@ -118,11 +145,16 @@ function logResult(kind, result, profundidade) {
118
145
  switch (kind) {
119
146
  case 'write_file':
120
147
  console.log(p + c.muted(`${result.created ? 'criado' : 'atualizado'} · ${tools.fmtBytes(result.bytes)}`));
148
+ // Reescrever arquivo que já existia apaga o que estava lá. O
149
+ // número de bytes não conta isso; o bloco embaixo conta.
150
+ mostrarDiff(result, profundidade);
121
151
  return;
122
- case 'edit_file': {
152
+ case 'edit_file':
153
+ case 'multi_edit': {
123
154
  const delta = result.linhasDepois - result.linhasAntes;
124
155
  const sinal = delta > 0 ? c.ok(`+${delta}`) : delta < 0 ? c.err(`${delta}`) : c.muted('±0');
125
156
  console.log(p + c.muted(`${result.substituicoes} troca${result.substituicoes > 1 ? 's' : ''} · `) + sinal + c.muted(' linhas'));
157
+ mostrarDiff(result, profundidade);
126
158
  return;
127
159
  }
128
160
  case 'read_file':
@@ -188,20 +220,6 @@ function logResult(kind, result, profundidade) {
188
220
  }
189
221
  return;
190
222
  }
191
- case 'buscar_audio': {
192
- console.log(p + c.muted(`${result.tipo === 'efeito' ? 'efeito' : 'música'}: `) + c.white(`${result.titulo}`)
193
- + c.muted(` — ${result.autor || 'autor desconhecido'}${result.duracao ? ` · ${result.duracao}s` : ''} · ${result.licenca}`));
194
- console.log(p + c.dim(`${result.url}`));
195
- return;
196
- }
197
- case 'marca_da_empresa': {
198
- const cores = (result.cores && result.cores.paleta || []).slice(0, 4).join(' ');
199
- console.log(p + c.white(result.nome || '') + c.muted(` · ${result.site || ''}`));
200
- console.log(p + c.dim(`cor ${(result.cores && result.cores.principal) || '—'}${cores ? ` · paleta ${cores}` : ''}`
201
- + `${(result.fontes || []).length ? ` · fonte ${result.fontes[0]}` : ''}`
202
- + `${result.logo ? ` · logo ${result.logo.formato} (${result.logo.fonte})` : ' · sem logo'}`));
203
- return;
204
- }
205
223
  case 'desktop_windows': {
206
224
  console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
207
225
  for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
@@ -249,7 +267,10 @@ function logResult(kind, result, profundidade) {
249
267
  // O que volta para o modelo. Cortamos saídas gigantes para não estourar a
250
268
  // janela de contexto na iteração seguinte.
251
269
  function summarizeForModel(result) {
252
- const json = JSON.stringify(result);
270
+ // As chaves com `_` são desenho de tela (as duas versões do arquivo, para
271
+ // o diff). Se escapassem daqui, cada edição mandaria o arquivo inteiro,
272
+ // duas vezes, de volta a um modelo que acabou de escrevê-lo.
273
+ const json = JSON.stringify(limparParaOModelo(result));
253
274
  return json.length > 6000 ? json.slice(0, 6000) + '…(truncado)' : json;
254
275
  }
255
276
 
@@ -369,23 +390,22 @@ async function pausar(ms, subagent) {
369
390
  *
370
391
  * 1. Há um plano (write_todos) com item por fazer — ele mesmo disse que
371
392
  * não acabou.
372
- * 2. O texto é INTENÇÃO: começa ou termina anunciando o próximo passo
373
- * ("vou", "em seguida", "próximo passo", dois-pontos no fim, reticências).
393
+ * 2. O texto é INTENÇÃO: anuncia o próximo passo ("vou", "em seguida",
394
+ * "próximo passo"), ou termina em dois-pontos e reticências.
374
395
  * 3. É curto. Uma resposta de verdade a "explique este erro" tem corpo; um
375
396
  * "vou abrir o arquivo agora" não tem.
376
397
  *
377
398
  * Pergunta ao usuário NÃO conta como parada: o prompt proíbe perguntar, mas
378
399
  * se o modelo perguntou, empurrá-lo faria ele decidir sozinho o que talvez
379
- * não devesse — e o texto de conversa ("oi, sou o PrimoCode") também não é
380
- * parada. Só intenção declarada e plano aberto.
400
+ * não devesse.
381
401
  */
382
- const INTENCAO = /(^|[.!\n]\s*)(agora\s+)?(vou|irei|deixa eu|deixe-me|preciso (agora )?(de )?(criar|ler|abrir|rodar|ver|conferir|pesquisar|mandar|fazer|gerar|escrever|ajustar)|primeiro (vou|eu)|em seguida|pr[óo]ximo passo|a seguir,?\s+(vou|eu)|vamos (criar|ler|abrir|rodar|ver|conferir|come[çc]ar|fazer|gerar|escrever))\b/i;
402
+ const INTENCAO = /(^|[.!\n]\s*)(agora\s+)?(vou|irei|deixa eu|deixe-me|preciso (agora )?(de )?(criar|ler|abrir|rodar|ver|conferir|pesquisar|mandar|fazer|gerar|escrever)|primeiro (vou|eu)|em seguida|pr[óo]ximo passo|a seguir,?\s+(vou|eu)|vamos (criar|ler|abrir|rodar|ver|conferir|come[çc]ar|fazer|gerar|escrever))\b/i;
383
403
  function pareceParadaNoMeio(texto, { todosPendentes = 0, rodouTool = false } = {}) {
384
404
  const t = String(texto || '').trim();
385
405
  if (!t) return false;
386
406
  if (todosPendentes > 0) return true;
387
- if (t.length > 700) return false; // resposta com corpo
388
- if (/\?\s*$/.test(t)) return false; // perguntou: não é parada
407
+ if (t.length > 700) return false; // resposta com corpo
408
+ if (/\?\s*$/.test(t)) return false; // perguntou: não é parada
389
409
  if (INTENCAO.test(t)) return true;
390
410
  if (rodouTool && /[:…]\s*$|\.\.\.\s*$/.test(t)) return true; // "Criando as cenas:" e nada depois
391
411
  return false;
@@ -422,6 +442,17 @@ async function runAct(userPrompt, opts) {
422
442
  const {
423
443
  server, model, token, history, projectDir, projectName,
424
444
  ensureWorkDir, profundidade = 0, subagent = false, effort,
445
+ /* Quem responde ao pedido de controle do computador, e o estado da
446
+ resposta. Vêm de fora porque quem sabe perguntar é o terminal, e
447
+ quem sabe lembrar é a SESSÃO — um subagente não pode começar do
448
+ zero e pedir de novo o que já foi autorizado. */
449
+ autorizar = null, controle = null,
450
+ /* O cliente HTTP, injetável. NÃO é mais um comutador de motor — o
451
+ PrimoCode tem um motor só, o do servidor, e não há provedor para o
452
+ usuário escolher. Continua um parâmetro porque é por aqui que o
453
+ teste põe um servidor de mentira no lugar da rede: sem ele,
454
+ `test/repeticao.js` e `test/continuar.js` teriam de falar com a
455
+ internet para verificar o loop do agente. */
425
456
  motor = api,
426
457
  } = opts || {};
427
458
 
@@ -436,6 +467,35 @@ async function runAct(userPrompt, opts) {
436
467
  let tentativasDeEncerrar = 0;
437
468
  // Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
438
469
  const jaChamado = repeticao.novo();
470
+ /* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
471
+ no fim como balanço (ver lib/registro.js). O subagente tem o seu, que
472
+ não se mistura com o do principal — mas o que ele fez também precisa
473
+ aparecer, então o de cima incorpora o de baixo no retorno. */
474
+ const livro = registro.novo();
475
+ /* A autorização do computador vale pela SESSÃO, então o estado vem de
476
+ fora. Um subagente que criasse o seu perguntaria de novo o que já foi
477
+ respondido — e uma pergunta repetida é uma pergunta que se responde
478
+ sem ler. Sem ninguém de fora (teste, pipe), nasce um aqui, que começa
479
+ fechado. */
480
+ const portaoControle = controle || permissao.novo();
481
+
482
+ /* AS SKILLS entram na frente do pedido, e só as que casam com ele.
483
+ *
484
+ * No pedido principal, não no subagente: o subagente já recebe o briefing
485
+ * do especialista dele, e empilhar os dois gastaria o teto de tokens
486
+ * duas vezes para dizer coisas parecidas.
487
+ *
488
+ * O que decide quais entram é o `quando:` de cada skill — ver
489
+ * lib/skills.js, que também explica por que mandar todas em todo pedido
490
+ * não é uma opção num plano limitado por token. */
491
+ let pedido = userPrompt;
492
+ if (!subagent) {
493
+ const comSkills = skills.montar(userPrompt);
494
+ pedido = comSkills.texto;
495
+ if (comSkills.skills.length) {
496
+ fala.pensamento(`seguindo: ${comSkills.skills.map((s) => s.titulo).join(', ')}`, profundidade);
497
+ }
498
+ }
439
499
  let voltasEmFalso = 0;
440
500
  // Pesquisas que voltaram de mãos vazias. A partir da terceira, a tool
441
501
  // para de rodar e devolve ORDEM DE SEGUIR: sem isto o agente entrava em
@@ -444,10 +504,10 @@ async function runAct(userPrompt, opts) {
444
504
  // assim que "crie um vídeo para a tostudy" morreu sem entregar nada.
445
505
  let pesquisasSecas = 0;
446
506
  let rodadasParadas = 0;
447
- // Para o `pareceParadaNoMeio`: alguma tool já rodou nesta tarefa, e o
448
- // último plano que o modelo escreveu (write_todos) — desta tarefa, não da
449
- // sessão: um plano velho de um pedido interrompido não pode empurrar o
450
- // pedido seguinte.
507
+ /* Para o `pareceParadaNoMeio`: se alguma tool já rodou nesta tarefa, e o
508
+ último plano que o modelo escreveu (write_todos) — desta TAREFA, não da
509
+ sessão: um plano velho, de um pedido interrompido, não pode empurrar o
510
+ pedido seguinte. */
451
511
  let rodouTool = false;
452
512
  let todosDaTarefa = [];
453
513
  let empurroes = 0;
@@ -460,32 +520,38 @@ async function runAct(userPrompt, opts) {
460
520
  if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
461
521
  }
462
522
  }
523
+ /* O que VAI para o modelo é `pedido` (com as skills na frente); o que
524
+ fica guardado na memória e no histórico é `userPrompt`, o que a pessoa
525
+ escreveu. Guardar o texto inflado faria a conversa crescer com o mesmo
526
+ conhecimento repetido em cada turno. */
463
527
  /* ── O GATILHO DO DIRETOR ────────────────────────────────────────────
464
528
  *
465
529
  * "A IA identifica que precisa criar vídeo, ativa o agente, esse agente
466
530
  * faz a estrutura e manda pra IA, e depois ela só faz o vídeo."
467
531
  *
468
532
  * Deixar isso só escrito no catálogo não basta: com um pedido curto
469
- * ("faz um vídeo do produto") o modelo pula direto para o studio_criar,
470
- * porque criar é o que ele sabe fazer. Quem reconhece o pedido é uma
471
- * regra (diretor.precisaDeProducao), e ela entra como ORDEM na conversa —
472
- * não como conselho no meio de cinquenta ferramentas.
533
+ * ("faz um vídeo do produto") o modelo pula direto para montar cena,
534
+ * porque montar cena é o que ele sabe fazer — e o roteiro, escrito no
535
+ * meio do JSON, vira legenda de slide. Quem reconhece o pedido é uma
536
+ * regra (diretor.precisaDeProducao), e ela entra como ORDEM na conversa,
537
+ * não como conselho perdido no meio de quarenta e sete ferramentas.
473
538
  *
474
539
  * Num subagente não entra: ele já está fazendo uma parte de um plano, e
475
540
  * um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
476
541
  if (!subagent && profundidade === 0) {
477
- const producao = require('./diretor.js').precisaDeProducao(userPrompt);
542
+ const producao = require('./diretor.js').precisaDeProducao(pedido);
478
543
  if (producao.producao) {
479
544
  messages.push({ role: 'system', content:
480
545
  'ESTE PEDIDO É DE PRODUÇÃO AUDIOVISUAL. Antes de criar qualquer peça, chame '
481
546
  + `planejar_producao com o pedido do usuário (formato "${producao.formato}"). `
482
- + 'Ele devolve a pauta — roteiro, narração falada, voz, câmera e a mídia a buscar — '
483
- + 'e é ela que você produz, cena a cena. Não comece pelo studio_criar: o roteiro '
484
- + 'escrito no meio do JSON da cena vira legenda de slide, que é o que se quer evitar.' });
547
+ + 'Ele devolve a pauta — arco, narração falada, voz, direção de arte e a mídia a buscar — '
548
+ + 'e é ela que você produz, cena a cena, compondo com as primitivas. Não comece pelo '
549
+ + 'estudio_plano: o roteiro escrito no meio do JSON da cena vira legenda de slide, '
550
+ + 'que é exatamente o que se quer evitar.' });
485
551
  }
486
552
  }
487
553
 
488
- messages.push({ role: 'user', content: userPrompt });
554
+ messages.push({ role: 'user', content: pedido });
489
555
 
490
556
  let finalText = '';
491
557
 
@@ -664,10 +730,10 @@ async function runAct(userPrompt, opts) {
664
730
  iteration--;
665
731
  continue;
666
732
  }
667
- // PAROU NO MEIO? Texto de intenção ("agora vou criar…") ou plano
668
- // com item aberto não é entrega. Em vez de encerrar, a tarefa é
669
- // devolvida: "isso foi uma promessa — faça". Três empurrões; se
670
- // depois disso ele insistir em só falar, a resposta é dele.
733
+ /* PAROU NO MEIO? Texto de intenção ("agora vou criar…") ou plano
734
+ com item aberto não é entrega. Em vez de encerrar, a tarefa
735
+ volta: "isso foi uma promessa — faça". Três empurrões; se
736
+ depois disso ele insistir em só falar, a resposta é dele. */
671
737
  const pendentes = todosDaTarefa.filter((t) => t.status !== 'completed').length;
672
738
  if (finalText && empurroes < MAX_EMPURROES && !PARAR
673
739
  && pareceParadaNoMeio(finalText, { todosPendentes: pendentes, rodouTool })) {
@@ -767,37 +833,34 @@ async function runAct(userPrompt, opts) {
767
833
  continue;
768
834
  }
769
835
 
770
- /* O PIPELINE DE QUATRO PAPÉIS mora aqui pelo mesmo motivo que o
771
- `spawn_agent`: ele PRECISA rodar agentes, e quem sabe rodar
772
- agente é este arquivo. Pôr isto no `tools.execute` obrigaria
773
- aquele módulo a conhecer o loop do agente — e um segundo lugar
774
- que sabe abrir conversa com o modelo é um segundo lugar para
775
- esquecer de passar o token, a pasta ou o motor. */
776
- if (tc.name === 'construir_app') {
836
+ /* O DIRETOR mora aqui pela mesma razão: ele É uma conversa
837
+ nova, e quem sabe abrir conversa é este arquivo. */
838
+ if (tc.name === 'planejar_producao') {
777
839
  algoRodou = true;
778
840
  fala.acao(tc.name, tc.arguments, profundidade);
779
- const r = await rodarPipeline(tc.arguments || {}, {
841
+ const r = await rodarDiretor(tc.arguments || {}, {
780
842
  server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
781
843
  });
782
844
  logResult(tc.name, r, profundidade);
783
845
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
784
- if (r && r.ok && catalogo.escreveArquivo(tc.name)) temPasta = true;
785
846
  continue;
786
847
  }
787
848
 
788
- /* O DIRETOR mora aqui pelo mesmo motivo do pipeline: ele roda
789
- um AGENTE, e quem sabe rodar agente é este arquivo. É a peça
790
- que o dono pediu — "a IA identifica que precisa criar vídeo,
791
- ativa o agente, esse agente faz a estrutura e manda pra IA, e
792
- depois ela só faz o vídeo". */
793
- if (tc.name === 'planejar_producao') {
849
+ /* O PIPELINE DE QUATRO PAPÉIS mora aqui pelo mesmo motivo que o
850
+ `spawn_agent`: ele PRECISA rodar agentes, e quem sabe rodar
851
+ agente é este arquivo. Pôr isto no `tools.execute` obrigaria
852
+ aquele módulo a conhecer o loop do agente — e um segundo lugar
853
+ que sabe abrir conversa com o modelo é um segundo lugar para
854
+ esquecer de passar o token ou a pasta. */
855
+ if (tc.name === 'construir_app') {
794
856
  algoRodou = true;
795
857
  fala.acao(tc.name, tc.arguments, profundidade);
796
- const r = await rodarDiretor(tc.arguments || {}, {
797
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
858
+ const r = await rodarPipeline(tc.arguments || {}, {
859
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, livro, autorizar, controle: portaoControle,
798
860
  });
799
861
  logResult(tc.name, r, profundidade);
800
862
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
863
+ if (r && r.ok && catalogo.escreveArquivo(tc.name)) temPasta = true;
801
864
  continue;
802
865
  }
803
866
 
@@ -834,12 +897,30 @@ async function runAct(userPrompt, opts) {
834
897
  continue;
835
898
  }
836
899
 
900
+ /* O PORTÃO do controle do computador. Antes da ação, não depois:
901
+ depois é o ponteiro já tendo andado. Ler a tela não passa por
902
+ aqui — ver é o que ele precisa fazer para saber onde clicar. */
903
+ if (permissao.precisa(tc.name)) {
904
+ const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
905
+ if (!porta.ok) {
906
+ fala.acao(tc.name, tc.arguments, profundidade);
907
+ console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
908
+ registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
909
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: porta.error }) });
910
+ continue;
911
+ }
912
+ }
913
+
837
914
  algoRodou = true;
838
915
  rodouTool = true;
839
916
  fala.acao(tc.name, tc.arguments, profundidade);
840
917
  const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
841
918
  logResult(tc.name, result, profundidade);
842
919
  if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
920
+ // Além de narrar, ANOTA: no fim do pedido sai o balanço do que
921
+ // nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
922
+ // tarefa de trinta ações, a primeira já saiu da tela.
923
+ registro.anotar(livro, tc.name, tc.arguments, result);
843
924
 
844
925
  // Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
845
926
  // arquivo. Escrever algo sem relação (ou um run_shell qualquer)
@@ -950,6 +1031,7 @@ async function runAct(userPrompt, opts) {
950
1031
  return {
951
1032
  userMessage: { role: 'user', content: userPrompt },
952
1033
  assistantMessage: { role: 'assistant', content: finalText },
1034
+ registro: livro,
953
1035
  // Esc Esc no meio de um plano de vários passos precisa parar o plano
954
1036
  // INTEIRO, não só o passo — senão o próximo passo começa logo depois
955
1037
  // de o usuário ter mandado parar.
@@ -978,8 +1060,16 @@ async function runSubagent(args, ctx) {
978
1060
  profundidade: profundidade + 1,
979
1061
  subagent: true,
980
1062
  motor,
1063
+ autorizar: ctx.autorizar,
1064
+ controle: ctx.controle, // o mesmo objeto: a resposta vale para todos
981
1065
  });
982
1066
 
1067
+ /* O que o subagente fez entra no livro de quem o chamou. Sem isto, o
1068
+ balanço no fim mentiria por omissão: o especialista escreveu cinco
1069
+ arquivos e a tela diria "nenhum arquivo". A conversa dele continua
1070
+ separada — o que se junta é o registro do que aconteceu no disco. */
1071
+ if (ctx.livro && r.registro) registro.juntar(ctx.livro, r.registro);
1072
+
983
1073
  const resumo = r.assistantMessage.content || 'sem resultado';
984
1074
  fala.agenteVoltou(resumo, profundidade);
985
1075
  return resumo;
@@ -1000,6 +1090,62 @@ async function runSubagent(args, ctx) {
1000
1090
  * coisa: quem acabou de escrever o código é o pior revisor dele, porque lê o
1001
1091
  * que quis escrever em vez do que escreveu.
1002
1092
  */
1093
+ /**
1094
+ * O DIRETOR: estrutura antes de alguém produzir.
1095
+ *
1096
+ * Por que uma conversa PRÓPRIA, e não mais um parágrafo no prompt de quem
1097
+ * produz: o modelo que está montando as cenas já gastou a atenção dele em
1098
+ * JSON — fonte, cor, duração, posição — e é ali que o roteiro morre. Vira
1099
+ * legenda do que aparece na tela ("Vantagens", "Nossos serviços"), sem arco,
1100
+ * sem narração escrita, sem decisão de voz. O diretor não vê JSON de cena
1101
+ * nenhum: ele decide o que se conta, em que ordem, em quantos segundos, com
1102
+ * que voz e com que direção de arte. Depois disso o produtor tem pouco a
1103
+ * inventar — e inventar é onde ele erra.
1104
+ */
1105
+ async function rodarDiretor(args, ctx) {
1106
+ const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
1107
+ if (profundidade > 0) {
1108
+ return { ok: false, error: 'Recusado: o diretor não roda dentro de um subagente. '
1109
+ + 'Produza com o plano que você já recebeu.' };
1110
+ }
1111
+ const diretor = require('./diretor.js');
1112
+ const correr = async (prompt, rotulo) => {
1113
+ if (PARAR) return '';
1114
+ fala.agente('Diretor', rotulo, profundidade);
1115
+ const r = await runAct(prompt, {
1116
+ server, model, token, projectDir, projectName,
1117
+ history: [], profundidade: profundidade + 1, subagent: true, motor,
1118
+ });
1119
+ return (r && r.assistantMessage && r.assistantMessage.content) || '';
1120
+ };
1121
+
1122
+ const r = await diretor.planejar({
1123
+ pedido: args.pedido || args.prompt || '',
1124
+ segundos: Number(args.segundos) || 30,
1125
+ formato: args.formato === 'apresentacao' ? 'apresentacao' : 'video',
1126
+ contexto: args.contexto || '',
1127
+ correr,
1128
+ mostrar: (t) => fala.agente('Diretor', t, profundidade),
1129
+ });
1130
+ if (!r.ok) return r;
1131
+
1132
+ /* A PAUTA é o que volta para o produtor, não o JSON cru. O JSON cru
1133
+ convida a copiar campo por campo para dentro do plano de cena — e aí o
1134
+ "fundo": "verde descendo" viraria uma string num lugar que espera cor.
1135
+ A pauta é prosa: o produtor LÊ e compõe com os primitivos que ele tem. */
1136
+ return {
1137
+ ok: true,
1138
+ aprovado: r.aprovado,
1139
+ defeitos: r.defeitos.length ? r.defeitos : undefined,
1140
+ pauta: diretor.pauta(r.plano),
1141
+ voz: r.plano.voz,
1142
+ midia: r.plano.midia,
1143
+ trilha: r.plano.trilha,
1144
+ segundos: r.plano.segundos,
1145
+ proporcao: r.plano.proporcao,
1146
+ };
1147
+ }
1148
+
1003
1149
  async function rodarPipeline(args, ctx) {
1004
1150
  const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
1005
1151
  if (profundidade > 0) {
@@ -1035,60 +1181,5 @@ async function rodarPipeline(args, ctx) {
1035
1181
  });
1036
1182
  }
1037
1183
 
1038
- /* ══ O DIRETOR ═════════════════════════════════════════════════════════════
1039
- *
1040
- * "Tenha uma IA específica pra roteiro que monta tudo — narração, qual voz,
1041
- * roteiro. Ela estrutura tudo antes de mandar pra IA."
1042
- *
1043
- * As regras, o prompt e a conferência do plano moram em `diretor.js`. Aqui é
1044
- * a costura: uma conversa NOVA para o diretor (ele não vê a do usuário nem a
1045
- * do produtor — é isso que faz o roteiro parar de virar legenda do slide) e a
1046
- * pauta de volta, que é o que o produtor recebe no lugar do pedido cru.
1047
- */
1048
- async function rodarDiretor(args, ctx) {
1049
- const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
1050
- const diretor = require('./diretor.js');
1051
-
1052
- const correr = async (prompt, rotulo) => {
1053
- if (PARAR) return '';
1054
- fala.agente('Diretor', rotulo, profundidade);
1055
- const r = await runAct(prompt, {
1056
- server, model, token, projectDir, projectName,
1057
- history: [], profundidade: profundidade + 1, subagent: true, motor,
1058
- });
1059
- return (r && r.assistantMessage && r.assistantMessage.content) || '';
1060
- };
1061
-
1062
- const r = await diretor.planejar({
1063
- pedido: args.pedido || args.prompt || '',
1064
- segundos: Number(args.segundos) || 30,
1065
- formato: args.formato === 'apresentacao' ? 'apresentacao' : 'video',
1066
- contexto: args.contexto || '',
1067
- correr,
1068
- mostrar: (linha) => console.log(fala.resultado(profundidade) + c.muted(linha)),
1069
- });
1070
- if (!r.ok) return r;
1071
-
1072
- return {
1073
- ok: true,
1074
- motor: r.plano.motor,
1075
- voz: r.plano.voz,
1076
- midia: r.plano.midia,
1077
- trilha: r.plano.trilha,
1078
- pauta: diretor.pauta(r.plano),
1079
- plano: r.plano,
1080
- ressalvas: r.defeitos,
1081
- aviso: 'A ESTRUTURA ESTÁ PRONTA. Agora só PRODUZA: busque a mídia listada com '
1082
- + 'midia_procurar + midia_baixar (use o "src" que volta em fundo.imagem e em src); '
1083
- + 'a TRILHA vem do buscar_audio (já baixa e credita) e a identidade de uma empresa, do marca_da_empresa. '
1084
- + `e monte com studio_criar (${r.plano.formato === 'apresentacao' ? 'prisma' : 'corte'}) `
1085
- + 'seguindo a pauta cena a cena. Não invente cena, não troque a ordem, não reescreva a narração. '
1086
- + (r.plano.motor.motor === 'remotion'
1087
- ? 'Depois de criar, gere o projeto Remotion com remotion_projeto: '
1088
- + r.plano.motor.motivo + '.'
1089
- : 'O motor é o estúdio: ' + r.plano.motor.motivo + '.'),
1090
- };
1091
- }
1092
-
1093
- module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, ehTransitorio, esperaDoErro,
1094
- anotacaoDoQueJaFoi, encolherParaContinuar, pareceParadaNoMeio, MAX_ITERATIONS, MAX_EMPURROES };
1184
+ module.exports = { runAct, interromper, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
1185
+ anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
package/lib/api.js CHANGED
@@ -220,6 +220,14 @@ function requestResumo(serverUrl, payload) {
220
220
  return request(serverUrl, '/api/resumir', body, token);
221
221
  }
222
222
 
223
+ /* A transcrição do áudio, com o tempo de cada trecho — é dela que sai a
224
+ legenda sincronizada. Quem tem a chave do provedor é o servidor, não o
225
+ CLI: essa é a promessa do produto. */
226
+ function requestTranscricao(serverUrl, payload) {
227
+ const { token, ...body } = payload;
228
+ return request(serverUrl, '/api/transcrever', body, token);
229
+ }
230
+
223
231
  // Refino do pedido: manda o texto cru, recebe a ordem de serviço.
224
232
  function requestRefino(serverUrl, payload) {
225
233
  const { token, ...body } = payload;
@@ -232,20 +240,43 @@ function requestPlano(serverUrl, payload) {
232
240
  return request(serverUrl, '/api/plano', body, token);
233
241
  }
234
242
 
243
+ /* Os conectores do servidor. Rota própria em vez de espremer do /health: o
244
+ /health carrega a corrente inteira dos três apelidos, e quem quer só saber
245
+ quem está de pé não precisa baixar isso. */
246
+ function getConectores(serverUrl, token) {
247
+ return pedirGet(serverUrl, '/api/conectores', token);
248
+ }
249
+
235
250
  function getHealth(serverUrl, token) {
251
+ return pedirGet(serverUrl, '/health', token);
252
+ }
253
+
254
+ function pedirGet(serverUrl, rota, token) {
236
255
  return new Promise((resolve, reject) => {
237
- const url = new URL('/health', serverUrl);
256
+ const url = new URL(rota, serverUrl);
238
257
  const client = url.protocol === 'https:' ? https : http;
239
258
  const req = client.get(buildOptions(url, 'GET', {}, null, token), (res) => {
240
259
  let body = '';
241
260
  res.on('data', (chunk) => { body += chunk; });
242
261
  res.on('end', () => {
243
262
  try { resolve(JSON.parse(body)); }
244
- catch { reject(new Error(`Resposta inválida do health check (HTTP ${res.statusCode})`)); }
263
+ catch { reject(new Error(`Resposta inválida de ${rota} (HTTP ${res.statusCode})`)); }
245
264
  });
246
265
  });
247
- req.on('error', reject);
266
+ /* Sem prazo, um servidor mudo pendura o comando para sempre — e
267
+ pendura calado, que é o pior jeito de travar.
268
+
269
+ Relógio de PAREDE, nunca `req.setTimeout`: aquele mede silêncio do
270
+ socket, e o silêncio de 40 segundos da máquina subindo no Render é
271
+ justamente o que não pode ser cortado. Aqui a conta é a resposta
272
+ inteira, que é o que de fato tem de acabar. */
273
+ const relogio = setTimeout(() => {
274
+ req.destroy(erroDeTempo('respondeu', PRAZO_RESPOSTA));
275
+ }, PRAZO_RESPOSTA);
276
+ if (relogio.unref) relogio.unref();
277
+ req.on('close', () => clearTimeout(relogio));
278
+ req.on('error', (e) => { clearTimeout(relogio); reject(e); });
248
279
  });
249
280
  }
250
281
 
251
- module.exports = { request, requestStream, requestAct, requestChat, requestResumo, requestRefino, requestPlano, getHealth };
282
+ module.exports = { request, requestStream, requestAct, requestChat, requestResumo, requestRefino, requestPlano, requestTranscricao, getHealth, getConectores };