primocode 8.42.2 → 9.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 (104) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  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 +42 -195
  7. package/lib/catalogo.js +100 -61
  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 +576 -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 +644 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +49 -6
  23. package/lib/marca.js +4 -1
  24. package/lib/markdown.js +376 -0
  25. package/lib/marketplace.js +189 -0
  26. package/lib/pasta.js +15 -4
  27. package/lib/permissao.js +137 -0
  28. package/lib/registro.js +229 -0
  29. package/lib/skills.js +329 -0
  30. package/lib/tools.js +106 -187
  31. package/lib/ui.js +7 -3
  32. package/lib/voz.js +239 -0
  33. package/package.json +12 -3
  34. package/skills/acessibilidade/SKILL.md +28 -0
  35. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  36. package/skills/depurar-de-verdade/SKILL.md +29 -0
  37. package/skills/editar-meu-video/SKILL.md +59 -0
  38. package/skills/escrever-em-portugues/SKILL.md +28 -0
  39. package/skills/git-limpo/SKILL.md +28 -0
  40. package/skills/motion-de-primeira/SKILL.md +62 -0
  41. package/skills/pagina-que-converte/SKILL.md +28 -0
  42. package/skills/performance-web/SKILL.md +28 -0
  43. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  44. package/skills/slides-que-prendem/SKILL.md +40 -0
  45. package/studio/edge.py +16 -66
  46. package/studio/narrar.py +274 -0
  47. package/studio/pessoa.py +199 -0
  48. package/voz/primo_voz/__init__.py +0 -0
  49. package/voz/primo_voz/__main__.py +383 -0
  50. package/voz/primo_voz/escuta.py +235 -0
  51. package/voz/primo_voz/orbe.py +216 -0
  52. package/lib/audio-livre.js +0 -177
  53. package/lib/chaves.js +0 -149
  54. package/lib/claude-engine.js +0 -631
  55. package/lib/codex-engine.js +0 -208
  56. package/lib/remotion.js +0 -663
  57. package/lib/studio.js +0 -960
  58. package/studio/PRIMOCODE.md +0 -58
  59. package/studio/README.md +0 -154
  60. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  61. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  62. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  63. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  64. package/studio/ferramentas/00-protocolo.md +0 -125
  65. package/studio/ferramentas/01-modelos.md +0 -270
  66. package/studio/ferramentas/README.md +0 -32
  67. package/studio/ferramentas/corte.md +0 -919
  68. package/studio/ferramentas/grade.md +0 -92
  69. package/studio/ferramentas/prisma.md +0 -107
  70. package/studio/ferramentas/prosa.md +0 -86
  71. package/studio/ferramentas/tela.md +0 -93
  72. package/studio/ferramentas/traco.md +0 -79
  73. package/studio/server.py +0 -2254
  74. package/studio/studio.log +0 -57
  75. package/studio/voz.py +0 -388
  76. package/studio/web/analise.html +0 -52
  77. package/studio/web/core.css +0 -1167
  78. package/studio/web/fontes/bebas.woff2 +0 -0
  79. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  80. package/studio/web/fontes/inter-400.woff2 +0 -0
  81. package/studio/web/fontes/inter-700.woff2 +0 -0
  82. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  83. package/studio/web/fontes/playfair-700.woff2 +0 -0
  84. package/studio/web/fontes.css +0 -23
  85. package/studio/web/home.html +0 -217
  86. package/studio/web/js/analise.js +0 -510
  87. package/studio/web/js/audio.js +0 -607
  88. package/studio/web/js/editor.js +0 -1802
  89. package/studio/web/js/exportar.js +0 -222
  90. package/studio/web/js/ferramentas.js +0 -108
  91. package/studio/web/js/icones.js +0 -120
  92. package/studio/web/js/midia.js +0 -157
  93. package/studio/web/js/narracao.js +0 -325
  94. package/studio/web/js/player.js +0 -903
  95. package/studio/web/js/render-grade.js +0 -243
  96. package/studio/web/js/render-prosa.js +0 -104
  97. package/studio/web/js/render.js +0 -1852
  98. package/studio/web/js/sfx.js +0 -233
  99. package/studio/web/js/video.js +0 -464
  100. package/studio/web/js/voz-neural.js +0 -152
  101. package/studio/web/render.css +0 -1352
  102. package/studio/web/tf.html +0 -15
  103. package/studio/web/tool.html +0 -78
  104. package/studio/web/view.html +0 -27
package/lib/tools.js CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  'use strict';
9
9
 
10
+ const catalogo = require('./catalogo.js');
11
+
10
12
  const fs = require('fs');
11
13
  const path = require('path');
12
14
  const os = require('os');
@@ -14,8 +16,7 @@ const { exec, execFile } = require('child_process');
14
16
  const browser = require('./browser');
15
17
  const desktop = require('./desktop');
16
18
  const referencia = require('./referencia');
17
- const studio = require('./studio');
18
- const acervo = require('./acervo');
19
+ const estudio = require('./estudio');
19
20
  const cfg = require('./config');
20
21
 
21
22
  const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
@@ -171,11 +172,6 @@ async function runShell(command, timeout = 60000, cwd) {
171
172
  // execFile com array de argumentos nunca passa por shell nenhum: "target" vira
172
173
  // UM argumento, não texto que um shell reinterpreta — sem isso, um valor tipo
173
174
  // `https://x/"; touch /tmp/pwned; echo "` injetava comando (achado de segurança).
174
- // Quando o auto-abrir do Studio abriu pela última vez — o freio das mil abas.
175
- let ultimaAberturaStudio = 0;
176
- // Uma rodada de melhoria por tarefa (ver o aviso em studio_criar).
177
- let melhorouStudio = false;
178
-
179
175
  function openInBrowser(target) {
180
176
  const p = os.platform();
181
177
  const [cmd, args, opener] = p === 'darwin' ? ['open', [target], 'open']
@@ -199,14 +195,43 @@ async function runInNewTerminal(command, cwd) {
199
195
  // mesmo tempo (osascript, cmd.exe, sh) — "command" continua livre de
200
196
  // propósito, é o próprio contrato desta tool (abrir terminal com um
201
197
  // comando de shell de verdade).
202
- if (/["'`;&|$\\]/.test(pastaAlvo)) {
198
+ /* A CONTRABARRA NÃO PODE ENTRAR NESTA LISTA NO WINDOWS.
199
+ Ela estava, e com isso a tool NUNCA funcionou lá: a pasta de trabalho é
200
+ sempre C:\Users\<nome>\primocode\<projeto> (path.join no win32 usa "\"),
201
+ então TODO caminho casava e a resposta era "Pasta com caractere não
202
+ permitido" — culpando a pasta criada pelo próprio produto, antes de
203
+ tentar abrir terminal nenhum. "sobe o servidor" quebrava 100% das vezes.
204
+ A contrabarra só é perigosa onde ela é ESCAPE (sh, osascript); no cmd.exe
205
+ ela é separador de caminho e nada mais. */
206
+ const proibidos = p === 'win32' ? /["'`;&|%^<>\r\n]/ : /["'`;&|$\\\r\n]/;
207
+ if (proibidos.test(pastaAlvo)) {
203
208
  return { ok: false, error: `Pasta com caractere não permitido: ${pastaAlvo}` };
204
209
  }
205
210
  let cmd;
206
- if (p === 'darwin') cmd = `osascript -e 'tell application "Terminal" to do script "cd ${pastaAlvo} && ${command}"'`;
207
- else if (p === 'win32') cmd = `start cmd /k "cd /d ${pastaAlvo} && ${command}"`;
208
- else cmd = `x-terminal-emulator -e "bash -c 'cd ${pastaAlvo} && ${command}; exec bash'"`;
209
- const r = await runShell(cmd);
211
+ let pastaDoProcesso;
212
+ if (p === 'darwin') {
213
+ /* O Terminal.app é outro processo, já rodando: ele NÃO herda a pasta
214
+ daqui, então o `cd` tem de ir no script mesmo. O espaço é escapado
215
+ com contrabarra — "\\ " neste template vira "\ " dentro da string do
216
+ AppleScript, que é o escape que o shell entende. Sem isso, uma pasta
217
+ como ~/Documents/Meus Projetos virava dois argumentos e o cd falhava
218
+ calado: o terminal abria na pasta errada e o comando rodava no lugar
219
+ errado, que é pior que não abrir. */
220
+ cmd = `osascript -e 'tell application "Terminal" to do script "cd ${pastaAlvo.replace(/ /g, '\\\\ ')} && ${command}"'`;
221
+ } else if (p === 'win32') {
222
+ // Aspas em volta do caminho: "C:\Users\Joel Silva\..." é comum, e sem
223
+ // elas o cmd corta no espaço. A aspa dupla já está barrada acima, então
224
+ // não há como sair delas.
225
+ cmd = `start cmd /k "cd /d "${pastaAlvo}" && ${command}"`;
226
+ } else {
227
+ /* No Linux o terminal é filho DESTE processo e herda a pasta — então
228
+ o caminho não precisa entrar na string de shell nenhuma. É a solução
229
+ mais segura das três: o que não é interpolado não pode ser mal
230
+ escapado. */
231
+ cmd = `x-terminal-emulator -e "bash -c '${command}; exec bash'"`;
232
+ pastaDoProcesso = pastaAlvo;
233
+ }
234
+ const r = await runShell(cmd, 60000, pastaDoProcesso);
210
235
  return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou' };
211
236
  }
212
237
 
@@ -270,6 +295,13 @@ function editFile({ path: rawPath, old_string, new_string, replace_all }, projec
270
295
  substituicoes: replace_all ? ocorrencias : 1,
271
296
  linhasAntes: original.split('\n').length,
272
297
  linhasDepois: atualizado.split('\n').length,
298
+ // As duas versões, para a TELA desenhar o diff. O `_` na frente marca
299
+ // o que é só nosso: o act.js apaga estas chaves antes de a conversa
300
+ // seguir, porque mandar o arquivo de volta ao modelo custaria
301
+ // centenas de tokens para ensinar a ele o texto que ele acabou de
302
+ // escrever.
303
+ _antes: original,
304
+ _depois: atualizado,
273
305
  };
274
306
  }
275
307
 
@@ -304,6 +336,7 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
304
336
  if (!Array.isArray(edits) || !edits.length) return { ok: false, error: 'edits (lista) é obrigatório' };
305
337
 
306
338
  let conteudo = fs.readFileSync(filePath, 'utf-8');
339
+ const original = conteudo;
307
340
  const linhasAntes = conteudo.split('\n').length;
308
341
  let substituicoes = 0;
309
342
 
@@ -329,6 +362,8 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
329
362
  substituicoes,
330
363
  linhasAntes,
331
364
  linhasDepois: conteudo.split('\n').length,
365
+ _antes: original,
366
+ _depois: conteudo,
332
367
  };
333
368
  }
334
369
 
@@ -401,9 +436,20 @@ async function despachar(name, args, ctx) {
401
436
  const filePath = resolveInProject(args.path, projectDir);
402
437
  const content = args.content || '';
403
438
  const created = !fs.existsSync(filePath);
439
+ // Reescrever um arquivo que já existe é a operação mais
440
+ // perigosa do agente: o conteúdo antigo some inteiro. Ler o
441
+ // que estava lá ANTES de escrever é o que permite mostrar na
442
+ // tela o que foi perdido.
443
+ let antes = '';
444
+ if (!created) { try { antes = fs.readFileSync(filePath, 'utf-8'); } catch { antes = ''; } }
404
445
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
405
446
  fs.writeFileSync(filePath, content, 'utf-8');
406
- return { ok: true, created, path: path.relative(projectDir, filePath).replace(/\\/g, '/'), bytes: content.length };
447
+ return {
448
+ ok: true, created,
449
+ path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
450
+ bytes: content.length,
451
+ ...(created ? {} : { _antes: antes, _depois: content }),
452
+ };
407
453
  }
408
454
  case 'edit_file':
409
455
  return editFile(args, projectDir);
@@ -441,16 +487,6 @@ async function despachar(name, args, ctx) {
441
487
  paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
442
488
  });
443
489
 
444
- // Música e efeito sonoro da internet, com licença e crédito. O
445
- // arquivo vem para a pasta do projeto e sobe para o estúdio;
446
- // volta a URL "/midia/…" pronta para trilha.src ou som.arquivo.
447
- case 'buscar_audio':
448
- return require('./audio-livre.js').buscarAudio(args, { projectDir, studio });
449
-
450
- // A logo e a identidade visual da empresa, lidas do site dela.
451
- case 'marca_da_empresa':
452
- return require('./marca.js').marcaDaEmpresa(args, { projectDir, studio });
453
-
454
490
  // memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
455
491
  // não alcançam por design (resolveInProject bloqueia ../ de propósito).
456
492
  // Esta é a única porta para lá, sem reabrir a sandbox.
@@ -514,171 +550,36 @@ async function despachar(name, args, ctx) {
514
550
  return desktop.scroll(args.dy != null ? args.dy : 300);
515
551
  case 'desktop_size':
516
552
  return desktop.getScreenSize();
517
- // ── PrimoCode Studio — a suíte de criação (sobe sozinha) ───────
518
- case 'studio_manual':
519
- return studio.manual();
520
- case 'studio_spec':
521
- return studio.spec(args.ferramenta || args.slug || '');
522
- case 'studio_criar': {
523
- const r = await studio.criar({ ferramenta: args.ferramenta, titulo: args.titulo, doc: args.doc });
524
- // Criou? ABRE NO NAVEGADOR, sozinho. O dono: "quando ele faz um
525
- // vídeo, uma apresentação, abre automático — a pessoa não vai
526
- // saber abrir". E cada "abre pra mim" digitado depois custava
527
- // uma tarefa inteira de tokens para o modelo redescobrir o
528
- // link. Abrir é determinístico: não se gasta modelo com isso.
529
- // Fica anotado na memória geral: é assim que "lembra do
530
- // vídeo tal" funciona na conversa seguinte. O id é o que
531
- // permite reabrir ou ajustar sem recriar.
532
- if (r && r.ok !== false && r.id) {
533
- cfg.lembrarCriacao({ ferramenta: r.ferramenta || args.ferramenta,
534
- titulo: r.titulo || args.titulo, id: r.id });
535
- }
536
- if (r && r.ok !== false && r.ver) {
537
- // UMA aba por tarefa. O modelo às vezes cria três versões
538
- // seguidas, e abrir todas enchia o navegador do usuário de
539
- // abas — "abre 1000 versões". A primeira abre; as outras
540
- // são avisadas a ATUALIZAR a que já existe.
541
- const agora = Date.now();
542
- if (agora - ultimaAberturaStudio > 45000) {
543
- ultimaAberturaStudio = agora;
544
- try { openInBrowser(r.ver); } catch {}
545
- r.abertoNoNavegador = true;
546
- // O estúdio revisa a peça e devolve o que ficou
547
- // monótono (mesmo fundo em tudo, transição repetida,
548
- // sem trilha). Sem este ramo o aviso mandaria "chame
549
- // finish, não ajuste" — e a revisão morreria ali, que
550
- // é justamente o "fica tudo genérico".
551
- //
552
- // UMA rodada de conserto, e acabou: mais que isso
553
- // vira o agente polindo sozinho enquanto o usuário
554
- // espera.
555
- if (Array.isArray(r.podeMelhorar) && r.podeMelhorar.length && !melhorouStudio) {
556
- melhorouStudio = true;
557
- r.aviso = 'JÁ ABERTO no navegador. Antes de finalizar, faça UMA correção: '
558
- + `use studio_atualizar no id "${r.id}" mandando SÓ as chaves que mudam, `
559
- + 'para resolver os pontos de "podeMelhorar". Depois chame finish. '
560
- + 'Não crie outro arquivo e não repita esta correção.';
561
- } else {
562
- r.aviso = 'JÁ ABERTO no navegador do usuário. Chame finish AGORA dizendo o que foi criado — não abra de novo, não leia de volta, não ajuste sem pedido.';
563
- }
564
- } else {
565
- r.aviso = `Você JÁ criou uma versão nesta tarefa, e ela está aberta no navegador. NÃO crie outra: `
566
- + `para mudar algo, use studio_atualizar com o id "${r.id}". Chame finish AGORA.`;
567
- }
568
- }
569
- return r;
570
- }
571
- case 'studio_ler':
572
- return studio.ler(args.id);
573
- case 'studio_atualizar':
574
- return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
575
- case 'studio_versoes':
576
- return studio.versoesDe({ id: args.id, restaurar: args.restaurar });
577
- case 'studio_listar':
578
- return studio.listar();
579
- case 'studio_midia':
580
- // Sem arquivo, lista o que já foi enviado; com arquivo, envia.
581
- return args.arquivo
582
- ? studio.midiaEnviar({ arquivo: resolveInProject(args.arquivo, projectDir), nome: args.nome })
583
- : studio.midiaListar();
584
- /* ── MÍDIA DE VERDADE, VINDA DA INTERNET ──────────────────
585
- *
586
- * Buscar, baixar, recortar o fundo e já deixar na galeria do
587
- * estúdio. O caminho inteiro é uma tool só do ponto de vista do
588
- * agente (procurar → baixar), e o `paraEstudio` é o que evita a
589
- * volta clássica: baixar num canto do disco e depois descobrir
590
- * que a cena precisa de uma URL "/midia/...".
591
- *
592
- * A licença desce junto e fica gravada ao lado do arquivo. Um
593
- * vídeo de cliente com foto CC BY sem crédito é problema do
594
- * cliente, e descobrir isso depois de publicar é tarde. */
595
- case 'midia_procurar':
596
- return acervo.procurar(args.consulta || args.q || '', {
597
- tipo: args.tipo, quantos: args.quantos, orientacao: args.orientacao,
598
- licenca: args.licenca,
599
- });
600
- case 'midia_baixar': {
601
- const r = await acervo.baixar(args.url, { titulo: args.titulo, meta: {
602
- autor: args.autor || null, licenca: args.licenca || null,
603
- fonte: args.fonte || null, pagina: args.pagina || null,
604
- } });
605
- if (!r.ok) return r;
606
- let arquivo = r.arquivo;
607
- if (args.semFundo) {
608
- const corte = await acervo.tirarFundo(arquivo);
609
- if (corte.ok) { arquivo = corte.arquivo; r.recorte = corte.metodo; r.avisoRecorte = corte.aviso; }
610
- else r.avisoRecorte = corte.error;
611
- }
612
- r.arquivo = arquivo;
613
- if (args.paraEstudio !== false) {
614
- const g = await studio.midiaEnviar({ arquivo, nome: args.nome });
615
- // O "/midia/..." é o que vai em `src` e em `fundo.imagem`.
616
- if (g && g.ok !== false) r.src = g.url || g.src || null;
617
- else r.avisoGaleria = (g && g.error) || 'não deu para mandar para a galeria';
618
- }
619
- return r;
620
- }
621
- case 'midia_sem_fundo':
622
- return acervo.tirarFundo(resolveInProject(args.arquivo, projectDir), { destino: args.destino });
623
- case 'midia_creditos':
624
- return { ok: true, creditos: acervo.creditos() };
625
-
626
- case 'studio_analisar': {
627
- // Ouvir o vídeo para achar as pausas. A conta acontece num
628
- // navegador (é quem sabe decodificar MP4 sem instalar nada) —
629
- // e ESCONDIDO: a conta não é da pessoa, e uma aba piscando no
630
- // navegador dela é encanamento aparecendo. Sem Chrome nem
631
- // Chromium nesta máquina, cai no navegador dela, como antes.
632
- const visivel = process.env.PRIMOCODE_ANALISE_VISIVEL === '1';
633
- const abrir = visivel ? openInBrowser : async (url) => {
634
- try { return await browser.abrirEscondido(url); }
635
- catch { openInBrowser(url); return null; }
636
- };
637
- return studio.analisarAudio({
638
- arquivo: args.arquivo, pausa: args.pausa, manter: args.manter,
639
- repetida: args.repetida, abrir, escondido: visivel ? null : browser.rodarEscondido,
640
- });
641
- }
642
- case 'studio_exportar':
643
- // O mp4 sem clique: a página do player roda escondida e gera
644
- // pelo mesmo caminho do botão. Ver lib/studio.js.
645
- // Com o Remotion mesclado: o vídeo quadro a quadro dele, o
646
- // áudio do Studio. PRIMOCODE_SEM_REMOTION=1 desliga.
647
- return studio.exportarVideo({
648
- id: args.id, destino: args.destino, fps: args.fps,
649
- escondido: browser.rodarEscondido, pasta: projectDir || process.cwd(),
650
- remotion: process.env.PRIMOCODE_SEM_REMOTION !== '1',
651
- });
652
-
653
- /* A MESMA peça, virando projeto Remotion.
654
- *
655
- * O Studio continua sendo o caminho normal: exporta em segundos,
656
- * sem instalar nada. Isto é para quando o usuário pede Remotion
657
- * pelo nome, ou quando a peça precisa do que só ele dá — render
658
- * determinístico, vídeo longo, fps e codec escolhidos, ou o
659
- * projeto em React para a pessoa mexer.
553
+ /* ── O ESTÚDIO ─────────────────────────────────────────────────
554
+ * Sete ferramentas, e nenhuma delas fala com servidor nenhum: o
555
+ * Studio antigo subia um Python em localhost:7777 porque tinha
556
+ * uma interface web para servir. A interface morreu — a IA faz
557
+ * tudo e abre o vídeo pronto — e o servidor morreu com ela.
660
558
  *
661
- * Parte do documento que já existe: nada de um segundo
662
- * vocabulário para o agente aprender. */
663
- case 'remotion_projeto': {
664
- const remotion = require('./remotion.js');
665
- const peca = await studio.ler(args.id);
666
- if (!peca || peca.ok === false) {
667
- return peca || { ok: false, error: `não achei a peça ${args.id}` };
668
- }
669
- const arq = peca.arquivo || peca;
670
- const base = projectDir || cfg.ROOT_DIR;
671
- const nome = cfg.slugify(args.pasta || arq.titulo || 'video') + '-remotion';
672
- const destino = path.join(base, nome);
673
- const r = remotion.montar({
674
- destino, titulo: arq.titulo, doc: arq.doc, fps: args.fps,
559
+ * Cada peça é uma PASTA, cada render é o Remotion. Ver
560
+ * lib/estudio/ para o porquê de cada peça.
561
+ */
562
+ case 'estudio_projeto':
563
+ return estudio.ferramentaProjeto(args);
564
+ case 'estudio_plano':
565
+ return estudio.ferramentaPlano(args);
566
+ case 'estudio_midia':
567
+ return estudio.ferramentaMidia(args);
568
+ case 'estudio_narrar':
569
+ return estudio.ferramentaNarrar(args);
570
+ case 'estudio_video':
571
+ // A transcrição precisa do servidor (a chave é dele), então o
572
+ // contexto do pedido atravessa até aqui.
573
+ return estudio.ferramentaVideo(args, ctx);
574
+ case 'estudio_render':
575
+ return estudio.ferramentaRender(args, {
576
+ ...ctx,
577
+ // O vídeo pronto abre para a pessoa assistir. É a única
578
+ // coisa que abre: foi o vídeo que ela pediu.
579
+ abrir: (arquivo) => { try { openInBrowser(arquivo); } catch {} },
675
580
  });
676
- if (!r.ok || !args.renderizar) return r;
677
- // Só renderiza quando pedido: o `npm install` demora minutos e
678
- // é escolha do usuário, não efeito colateral de criar a pasta.
679
- const feito = await remotion.renderizar({ pasta: destino });
680
- return Object.assign(r, { render: feito });
681
- }
581
+ case 'estudio_site':
582
+ return estudio.ferramentaSite(args);
682
583
 
683
584
  case 'desktop_double_click':
684
585
  return desktop.doubleClick(args.x, args.y);
@@ -698,7 +599,25 @@ async function despachar(name, args, ctx) {
698
599
  });
699
600
 
700
601
  default:
701
- return { ok: false, error: `Tool não implementada: ${name}` };
602
+ /* O MODELO PRECISA CONSEGUIR SE CORRIGIR AQUI.
603
+ "Tool não implementada: studio_criar" não diz o que existe,
604
+ e o que acontecia era o agente desistir e responder ao
605
+ usuário que a ferramenta necessária "não está disponível".
606
+ Dizendo QUAIS existem — e a mais parecida — ele chama a
607
+ certa na volta seguinte, na mesma tarefa.
608
+
609
+ Isto é rede, não conserto: a causa é a lista do servidor
610
+ divergir do catálogo daqui, e ela se conserta lá. Mas a rede
611
+ é o que separa uma tarefa que se recupera de uma que morre
612
+ com o usuário esperando. */
613
+ return {
614
+ ok: false,
615
+ error: `A ferramenta "${name}" não existe.`,
616
+ parecidas: catalogo.parecidasCom(name),
617
+ comoResolver: 'chame uma das ferramentas que existem de verdade — as "parecidas" '
618
+ + 'acima são as de nome mais próximo. Não invente nome de ferramenta, e não '
619
+ + 'desista da tarefa por causa disto.',
620
+ };
702
621
  }
703
622
  }
704
623
  }
package/lib/ui.js CHANGED
@@ -160,9 +160,10 @@ const TIPS = [
160
160
  ];
161
161
 
162
162
  const NEWS = [
163
- 'Vídeo com DIRETOR: roteiro, narração, voz e movimento antes de produzir',
164
- 'Ele busca e baixa imagem, logo e música — com a licença junto',
165
- 'A escolha da IA na abertura agora anda nas setinhas',
163
+ 'Vídeo com DIRETOR: roteiro, narração e voz decididos antes de produzir',
164
+ 'Ele busca e baixa imagem, logo e música — com a licença junto do arquivo',
165
+ '/voice abre o Primo Code Voice · /marketplace instala skills',
166
+ 'Do "high" para cima ele PESQUISA referências antes de escrever',
166
167
  'Esc Esc a qualquer momento para o agente no meio do trabalho',
167
168
  ];
168
169
 
@@ -313,6 +314,9 @@ function actionHeader(name, args) {
313
314
  const label = ROTULOS[name] || name;
314
315
  let argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.ferramenta || args.titulo || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || args.arquivo || args.id || JSON.stringify(args).slice(0,60)) : '';
315
316
  argStr = String(argStr || '').replace(/\s+/g, ' ').trim();
317
+ // Uma tool sem argumento nenhum caía no JSON.stringify e desenhava "{}"
318
+ // ao lado do nome — ruído que não diz nada.
319
+ if (argStr === '{}' || argStr === 'null' || argStr === 'undefined') argStr = '';
316
320
  if (argStr.length > 96) argStr = argStr.slice(0, 95) + '…';
317
321
  return ` ${c.brand('●')} ${c.bold(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
318
322
  }
package/lib/voz.js ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * voz.js — o lado Node do modo voz.
3
+ *
4
+ * O aplicativozinho da orb é Python (ver voz/primo_voz). O que mora aqui é a
5
+ * outra ponta: subir o processo, conversar com ele por JSON, e — a parte que
6
+ * importa — entregar cada pedido ao MESMO agente do terminal.
7
+ *
8
+ * ── POR QUE O CÉREBRO NÃO FOI PARA O PYTHON ──────────────────────────────
9
+ * O Jarvis mantinha uma sessão do Claude Code num processo filho e conversava
10
+ * por stream-json. Copiar aquele desenho aqui significaria uma segunda
11
+ * implementação do laço do agente, das quarenta ferramentas, do portão de
12
+ * permissão do computador e da corrente de conectores — em outra linguagem.
13
+ *
14
+ * Duas implementações do mesmo laço é a garantia de que uma delas está errada,
15
+ * e de que a correção de amanhã vai para só uma. Então o modo voz manda o
16
+ * texto para o `runAct` de sempre: as mesmas ferramentas, as mesmas skills, a
17
+ * mesma permissão, os mesmos conectores.
18
+ *
19
+ * ── O PEDIDO SAI DIFERENTE, E SÓ ISSO ────────────────────────────────────
20
+ * Uma resposta boa de ler é ruim de ouvir: lista com marcador, caminho de
21
+ * arquivo, bloco de código e trecho longo só funcionam para o olho. Então o
22
+ * pedido vai com uma instrução curta de FORMA — não de conteúdo. O agente
23
+ * continua fazendo o mesmo trabalho; ele só conta de outro jeito.
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ const fs = require('fs');
29
+ const os = require('os');
30
+ const path = require('path');
31
+ const { spawn, execFileSync } = require('child_process');
32
+
33
+ const PASTA = path.join(__dirname, '..', 'voz');
34
+
35
+ /* A instrução de FORMA. Ela não muda o que o agente faz — muda como ele
36
+ conta. O que está proibido aqui é o que não existe em voz: marcador,
37
+ caminho, código, e parágrafo que ninguém segura de ouvido. */
38
+ const COMO_FALAR = `[MODO VOZ] A sua resposta vai ser LIDA EM VOZ ALTA para a pessoa, e ela não vê tela nenhuma.
39
+
40
+ Responda em no máximo 3 frases curtas, em português falado. Nada de lista com
41
+ marcador, nada de caminho de arquivo, nada de bloco de código, nada de
42
+ markdown — nada disso existe no ouvido de alguém.
43
+
44
+ Diga o que você FEZ e o que mudou, não como. Se algo deu errado, diga em uma
45
+ frase o que foi e o que você precisa.
46
+
47
+ Faça o trabalho inteiro normalmente, com as ferramentas de sempre. O que muda
48
+ é só o jeito de contar no fim.`;
49
+
50
+ /** Onde está o Python 3. Mesmo problema do Estúdio, mesma cura. */
51
+ function python() {
52
+ const nomes = process.platform === 'win32'
53
+ ? ['python', 'py', 'python3']
54
+ : ['python3', 'python', '/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3'];
55
+ for (const n of nomes) {
56
+ try {
57
+ const v = execFileSync(n, ['--version'], { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 }).toString();
58
+ if (/Python 3/.test(v)) return n;
59
+ } catch { /* próximo */ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * O que falta para o modo voz abrir — e QUAL é a falta.
66
+ *
67
+ * A distinção é a diferença entre um recado que resolve e um que faz a pessoa
68
+ * dar voltas. Dois casos não se resolvem com pip:
69
+ *
70
+ * · `sounddevice` instalado, mas sem o PortAudio do sistema. O import
71
+ * explode com "PortAudio library not found", e mandar `pip install
72
+ * sounddevice` ali é mandar instalar o que já está instalado.
73
+ * · `tkinter` vem com o Python, mas algumas distribuições Linux o separam
74
+ * num pacote do sistema — e `pip install tkinter` não existe.
75
+ *
76
+ * Por isso a checagem lê a MENSAGEM do erro, e não só o fato de ter falhado.
77
+ */
78
+ function faltando(py) {
79
+ const falta = [];
80
+ for (const [modulo, pacote] of [['numpy', 'numpy'], ['sounddevice', 'sounddevice'], ['tkinter', null]]) {
81
+ try {
82
+ execFileSync(py, ['-c', `import ${modulo}`], { stdio: ['ignore', 'pipe', 'pipe'], timeout: 20000 });
83
+ } catch (e) {
84
+ const saida = String((e.stderr || '') + (e.stdout || ''));
85
+ if (/portaudio/i.test(saida)) {
86
+ falta.push({ modulo, pacote: null, sistema: true, porque: 'o PortAudio, que é biblioteca do sistema' });
87
+ } else {
88
+ falta.push({ modulo, pacote });
89
+ }
90
+ }
91
+ }
92
+ return falta;
93
+ }
94
+
95
+ /** Como instalar uma biblioteca de sistema, neste sistema. */
96
+ const COMO_SISTEMA = {
97
+ portaudio: {
98
+ darwin: 'brew install portaudio',
99
+ linux: 'sudo apt install libportaudio2 (ou o equivalente da sua distribuição)',
100
+ win32: 'no Windows ele vem junto com o sounddevice — reinstale com pip',
101
+ },
102
+ tkinter: {
103
+ darwin: 'brew install python-tk',
104
+ linux: 'sudo apt install python3-tk',
105
+ win32: 'reinstale o Python pelo python.org, marcando "tcl/tk"',
106
+ },
107
+ };
108
+
109
+ /**
110
+ * Confere o que é preciso, sem instalar nada.
111
+ *
112
+ * O tkinter é o caso especial: ele vem com o Python, mas em algumas
113
+ * distribuições Linux ele foi separado num pacote do SISTEMA — e `pip install
114
+ * tkinter` não existe. Dizer "instale com pip" ali seria mandar a pessoa a um
115
+ * lugar onde não há nada.
116
+ */
117
+ function conferir() {
118
+ const py = python();
119
+ if (!py) {
120
+ return { ok: false, error: 'o modo voz precisa do Python 3, e não achei nenhum nesta máquina.',
121
+ comoResolver: 'instale em python.org e abra o terminal de novo' };
122
+ }
123
+ const falta = faltando(py);
124
+ if (!falta.length) return { ok: true, python: py };
125
+
126
+ const pip = falta.filter((f) => f.pacote).map((f) => f.pacote);
127
+ const linhas = [];
128
+ if (pip.length) linhas.push(`${py} -m pip install ${pip.join(' ')}`);
129
+ if (falta.some((f) => f.modulo === 'sounddevice' && f.sistema)) {
130
+ linhas.push(COMO_SISTEMA.portaudio[process.platform] || COMO_SISTEMA.portaudio.linux);
131
+ }
132
+ if (falta.some((f) => f.modulo === 'tkinter')) {
133
+ linhas.push(COMO_SISTEMA.tkinter[process.platform] || COMO_SISTEMA.tkinter.linux);
134
+ }
135
+
136
+ return {
137
+ ok: false,
138
+ python: py,
139
+ falta: falta.map((f) => f.modulo),
140
+ error: `falta o que o modo voz precisa: ${falta.map((f) => f.porque || f.modulo).join(', ')}`,
141
+ comoResolver: linhas.join('\n '),
142
+ // Só o que o pip resolve entra no "instalar agora": oferecer instalar
143
+ // o que ele não instala é prometer o que não se cumpre.
144
+ instalavel: pip,
145
+ };
146
+ }
147
+
148
+ /** Instala o que dá para instalar com pip. O tkinter não passa por aqui. */
149
+ function instalar(py, pacotes, aoFalar = () => {}) {
150
+ return new Promise((resolve) => {
151
+ aoFalar(`instalando ${pacotes.join(', ')} (uma vez, alguns minutos)`);
152
+ const p = spawn(py, ['-m', 'pip', 'install', '--quiet', ...pacotes]);
153
+ let erro = '';
154
+ p.stderr.on('data', (d) => { erro += d.toString(); });
155
+ p.on('close', (code) => resolve(code === 0
156
+ ? { ok: true }
157
+ : { ok: false, error: erro.slice(-400).trim() || `pip saiu com ${code}` }));
158
+ p.on('error', (e) => resolve({ ok: false, error: e.message }));
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Abre o modo voz e fica de pé até a janela fechar.
164
+ *
165
+ * @param {object} op
166
+ * @param {string} op.python
167
+ * @param {string} op.servidor
168
+ * @param {string} op.token
169
+ * @param {string} op.voz
170
+ * @param {(texto:string)=>Promise<string>} op.pensar o agente de verdade
171
+ * @param {(texto:string)=>void} op.mostrar o que aparece no terminal
172
+ */
173
+ function abrir({ python: py, servidor, token, voz, pensar, mostrar = () => {} }) {
174
+ return new Promise((resolve) => {
175
+ const p = spawn(py, ['-m', 'primo_voz'], {
176
+ cwd: PASTA,
177
+ env: {
178
+ ...process.env,
179
+ PYTHONPATH: PASTA,
180
+ PRIMOCODE_SERVER: servidor,
181
+ PRIMOCODE_TOKEN: token || '',
182
+ PRIMOCODE_VOZ: voz || 'antonio',
183
+ PYTHONUNBUFFERED: '1',
184
+ },
185
+ });
186
+
187
+ const manda = (obj) => {
188
+ try { p.stdin.write(JSON.stringify(obj) + '\n'); } catch { /* já fechou */ }
189
+ };
190
+
191
+ let buffer = '';
192
+ let ocupado = false;
193
+
194
+ p.stdout.on('data', async (d) => {
195
+ buffer += d.toString();
196
+ const linhas = buffer.split('\n');
197
+ buffer = linhas.pop();
198
+ for (const linha of linhas) {
199
+ if (!linha.trim()) continue;
200
+ let msg;
201
+ try { msg = JSON.parse(linha); } catch { continue; }
202
+
203
+ if (msg.tipo === 'fim') { try { p.kill(); } catch {} return; }
204
+ if (msg.tipo === 'erro') { mostrar(String(msg.texto || '')); continue; }
205
+ if (msg.tipo !== 'pedido') continue;
206
+
207
+ /* Um pedido por vez. A pessoa pode falar de novo enquanto ele
208
+ trabalha — e dois agentes rodando ao mesmo tempo na mesma
209
+ pasta é a receita para um escrever por cima do outro. */
210
+ if (ocupado) { manda({ tipo: 'resposta', texto: 'Ainda estou terminando o anterior.' }); continue; }
211
+ ocupado = true;
212
+
213
+ mostrar(String(msg.texto || ''));
214
+ manda({ tipo: 'estado', estado: 'pensando' });
215
+ try {
216
+ const resposta = await pensar(String(msg.texto || ''));
217
+ manda({ tipo: 'resposta', texto: resposta || 'Feito.' });
218
+ } catch (e) {
219
+ manda({ tipo: 'erro', texto: `Não consegui: ${e.message}` });
220
+ } finally {
221
+ ocupado = false;
222
+ }
223
+ }
224
+ });
225
+
226
+ // O Python fala com o terminal pela saída de ERRO, de propósito: o
227
+ // stdout inteiro é do protocolo, e uma linha solta ali quebraria a
228
+ // conversa entre os dois processos.
229
+ p.stderr.on('data', (d) => {
230
+ const texto = d.toString().trim();
231
+ if (texto) mostrar(texto, true);
232
+ });
233
+
234
+ p.on('close', () => resolve({ ok: true }));
235
+ p.on('error', (e) => resolve({ ok: false, error: e.message }));
236
+ });
237
+ }
238
+
239
+ module.exports = { conferir, instalar, abrir, python, COMO_FALAR, PASTA };