primocode 8.35.0 → 8.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/act.js CHANGED
@@ -616,6 +616,24 @@ async function runAct(userPrompt, opts) {
616
616
  continue;
617
617
  }
618
618
 
619
+ /* O PIPELINE DE QUATRO PAPÉIS mora aqui pelo mesmo motivo que o
620
+ `spawn_agent`: ele PRECISA rodar agentes, e quem sabe rodar
621
+ agente é este arquivo. Pôr isto no `tools.execute` obrigaria
622
+ aquele módulo a conhecer o loop do agente — e um segundo lugar
623
+ que sabe abrir conversa com o modelo é um segundo lugar para
624
+ esquecer de passar o token, a pasta ou o motor. */
625
+ if (tc.name === 'construir_app') {
626
+ algoRodou = true;
627
+ console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
628
+ const r = await rodarPipeline(tc.arguments || {}, {
629
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
630
+ });
631
+ logResult(tc.name, r, profundidade);
632
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
633
+ if (r && r.ok && catalogo.escreveArquivo(tc.name)) temPasta = true;
634
+ continue;
635
+ }
636
+
619
637
  // Repetiu a mesma chamada? Não executa de novo. Rodar outra vez
620
638
  // gastaria a cota para devolver exatamente o mesmo texto, e é
621
639
  // esse texto que convence o modelo a repetir mais uma vez.
@@ -798,5 +816,55 @@ async function runSubagent(args, ctx) {
798
816
  return resumo;
799
817
  }
800
818
 
819
+ /* ══ O PIPELINE DE QUATRO PAPÉIS ═══════════════════════════════════════════
820
+ *
821
+ * "Engenheiro de Prompt → Construtor 1 (protótipo) → Construtor 2 (MVP) →
822
+ * Revisador (testa ocultamente) → loop de correção → abre no navegador."
823
+ *
824
+ * As regras dos papéis, os prompts e o revisor que abre a página moram em
825
+ * `pipeline.js`, que não conhece o loop do agente. O que esta função faz é a
826
+ * costura: dar a ele uma forma de RODAR um agente, uma de escrever na tela e
827
+ * uma de abrir o navegador da pessoa.
828
+ *
829
+ * Cada papel roda com `history: []` — conversa própria, sem ver a do usuário
830
+ * nem a do papel anterior. É essa separação que faz o revisor valer alguma
831
+ * coisa: quem acabou de escrever o código é o pior revisor dele, porque lê o
832
+ * que quis escrever em vez do que escreveu.
833
+ */
834
+ async function rodarPipeline(args, ctx) {
835
+ const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
836
+ if (profundidade > 0) {
837
+ return { ok: false, error: 'Recusado: o pipeline não roda dentro de um subagente — '
838
+ + 'os papéis dele já são subagentes. Faça a parte que te cabe.' };
839
+ }
840
+
841
+ const pipeline = require('./pipeline.js');
842
+ const path = require('path');
843
+ const pedido = args.pedido || args.prompt || '';
844
+ const base = projectDir || cfg.ROOT_DIR;
845
+ const pasta = args.pasta
846
+ ? path.resolve(base, args.pasta)
847
+ : pipeline.pastaPara(pedido, base);
848
+
849
+ const correr = async (prompt, rotulo) => {
850
+ if (PARAR) return '';
851
+ console.log(ident(profundidade) + ` ${c.magenta('◆')} ${c.bold(c.magenta('Agente'))} ${c.muted(rotulo)}`);
852
+ const r = await runAct(prompt, {
853
+ server, model, token, projectDir: pasta, projectName,
854
+ history: [], profundidade: profundidade + 1, subagent: true, motor,
855
+ });
856
+ return (r && r.assistantMessage && r.assistantMessage.content) || '';
857
+ };
858
+
859
+ return pipeline.construir({
860
+ pedido,
861
+ pasta,
862
+ rodadas: args.rodadas,
863
+ correr,
864
+ mostrar: (linha) => console.log(ident(profundidade) + ` ${c.muted('⎿ ' + linha)}`),
865
+ abrir: tools.openInBrowser,
866
+ });
867
+ }
868
+
801
869
  module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, esperaDoErro,
802
870
  anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
package/lib/browser.js CHANGED
@@ -27,6 +27,36 @@ const PROFILE_DIR = path.join(os.homedir(), '.primocode', 'chrome-profile');
27
27
  const DEBUG_PORT = Number(process.env.PRIMOCODE_CDP_PORT || 3199);
28
28
 
29
29
  // ---- Helpers ---------------------------------------------------
30
+ /* O Chromium que o Playwright baixa. Ele não fica no PATH nem em /Applications
31
+ — mora numa pasta com o número da build no nome, que muda a cada versão —,
32
+ então achá-lo é olhar a pasta e escolher a mais nova. */
33
+ function doPlaywright() {
34
+ const bases = [
35
+ process.env.PLAYWRIGHT_BROWSERS_PATH,
36
+ path.join(os.homedir(), '.cache', 'ms-playwright'),
37
+ path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright'),
38
+ process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'ms-playwright'),
39
+ ].filter(Boolean);
40
+ const dentro = process.platform === 'darwin'
41
+ ? ['chrome-mac/Chromium.app/Contents/MacOS/Chromium']
42
+ : process.platform === 'win32'
43
+ ? ['chrome-win\\chrome.exe']
44
+ : ['chrome-linux/chrome', 'chrome-linux/headless_shell'];
45
+ for (const base of bases) {
46
+ let pastas = [];
47
+ try { pastas = fs.readdirSync(base).filter((d) => d.startsWith('chromium')); } catch { continue; }
48
+ // A build mais nova primeiro: "chromium-1194" ganha de "chromium-1080".
49
+ pastas.sort((a, b) => (parseInt(b.split('-').pop(), 10) || 0) - (parseInt(a.split('-').pop(), 10) || 0));
50
+ for (const p of pastas) {
51
+ for (const d of dentro) {
52
+ const cheio = path.join(base, p, d);
53
+ try { if (fs.existsSync(cheio)) return cheio; } catch { /* próximo */ }
54
+ }
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
30
60
  function findChrome() {
31
61
  const platform = process.platform;
32
62
  // 1) Respeita a variável de ambiente — aponta para qualquer Chrome/Chromium.
@@ -57,6 +87,11 @@ function findChrome() {
57
87
  for (const p of candidates) {
58
88
  try { if (p && fs.existsSync(p)) return p; } catch {}
59
89
  }
90
+ // Quem já tem Playwright instalado tem um Chromium no disco e não sabe o
91
+ // caminho. Sem esta busca, a máquina com navegador é tratada como máquina
92
+ // sem navegador — e o revisor recusa trabalhar por nada.
93
+ const dosTestes = doPlaywright();
94
+ if (dosTestes) return dosTestes;
60
95
 
61
96
  // Último recurso: o navegador pode estar num disco montado ou numa pasta de
62
97
  // aplicativos do usuário. Caso real medido nesta máquina: o Chrome 151 estava
@@ -295,6 +330,12 @@ class CDPSession {
295
330
  this.msgId = 0;
296
331
  this.pending = new Map(); // id -> { resolve, reject }
297
332
  this.targetId = null;
333
+ /* Quem quiser ouvir EVENTO do navegador (erro de JS, console, recurso
334
+ que não carregou) põe uma função aqui. Ela nasceu para o revisor
335
+ escondido: o defeito que ele existe para achar — a página que abre em
336
+ branco por causa de um TypeError — não aparece em resposta nenhuma de
337
+ comando, só em `Runtime.exceptionThrown`. */
338
+ this.onEvent = null;
298
339
  }
299
340
  async attach(wsUrl) {
300
341
  this.ws = new MiniWebSocket();
@@ -310,8 +351,11 @@ class CDPSession {
310
351
  this.pending.delete(msg.id);
311
352
  if (msg.error) reject(new Error(msg.error.message || 'erro CDP'));
312
353
  else resolve(msg.result || {});
354
+ return;
355
+ }
356
+ if (msg.method && this.onEvent) {
357
+ try { this.onEvent(msg.method, msg.params || {}); } catch { /* ouvinte não derruba a sessão */ }
313
358
  }
314
- // eventos são ignorados (não usamos aqui)
315
359
  }
316
360
  send(method, params = {}, timeoutMs = 15000) {
317
361
  const id = ++this.msgId;
@@ -644,7 +688,254 @@ function close() {
644
688
  return { ok: true };
645
689
  }
646
690
 
691
+ /* ══ O REVISOR ESCONDIDO ═══════════════════════════════════════════════════
692
+ *
693
+ * "Revisador: testa o código ocultamente, identifica erros e reporta ao
694
+ * Construtor 2."
695
+ *
696
+ * ── POR QUE UM SEGUNDO NAVEGADOR ─────────────────────────────────────────
697
+ * O daqui de cima é o navegador DA PESSOA: mesmo perfil, mesmos logins, e ela
698
+ * está olhando. Um revisor que rodasse ali abriria abas, navegaria e fecharia
699
+ * páginas no meio do trabalho de quem está usando o computador — e o pedido é
700
+ * justamente que ele teste SEM aparecer. Então é outro processo: porta
701
+ * própria, perfil próprio, sempre `--headless`, e ele nasce e morre com a
702
+ * revisão.
703
+ *
704
+ * ── O QUE ELE PROCURA ────────────────────────────────────────────────────
705
+ * O defeito que ele existe para achar não dá erro em lugar nenhum: a página
706
+ * abre, responde 200, tem título, e está EM BRANCO — um `TypeError` na
707
+ * primeira linha do script, um `<script src>` que não existe, uma tag mal
708
+ * fechada. Quem escreveu não vê, porque o modelo não abre o que escreveu; e
709
+ * quem pediu vê uma tela vazia e conclui que o PrimoCode não funciona.
710
+ *
711
+ * Por isso a leitura é de DUAS naturezas, e as duas são necessárias:
712
+ *
713
+ * · O QUE O NAVEGADOR RECLAMOU — exceção de JavaScript, erro de console,
714
+ * recurso que não carregou. É o sintoma direto e vem por evento, nunca
715
+ * como resposta de comando.
716
+ * · O QUE FICOU NA TELA — quantos elementos têm área de verdade e quanto
717
+ * texto visível existe. Página sem erro nenhum e sem nada dentro é o caso
718
+ * mais comum de todos, e sem esta metade ele aprovaria.
719
+ */
720
+
721
+ /* ── PORTA E PERFIL SÃO DESTA EXECUÇÃO, e isso é conserto de defeito ────
722
+ *
723
+ * A primeira versão usava porta fixa (3198) e um perfil fixo em
724
+ * `~/.primocode/chrome-revisor`. Duas sessões do PrimoCode na mesma máquina —
725
+ * duas abas de terminal, ou a suíte de testes rodando enquanto alguém trabalha
726
+ * — disputavam os dois: a segunda achava a porta ocupada e se ligava ao
727
+ * navegador da PRIMEIRA, e quando qualquer uma terminasse, `fecharRevisor`
728
+ * matava o navegador da outra no meio de uma revisão. O sintoma foi medido:
729
+ * "não consegui abrir a página para testar", intermitente, sem causa visível.
730
+ *
731
+ * Porta ZERO é o Chrome escolhendo uma livre e escrevendo qual no arquivo
732
+ * `DevToolsActivePort` do perfil — é o jeito dele de responder isso, e não há
733
+ * corrida como haveria em pedir uma porta ao sistema e depois lançar.
734
+ *
735
+ * O perfil é temporário porque o revisor não tem o que guardar: ele não faz
736
+ * login em lugar nenhum, e um perfil que fica só acumula disco. */
737
+ let PORTA_REVISOR = Number(process.env.PRIMOCODE_CDP_PORT_REVISOR || 0);
738
+ let perfilRevisor = null;
739
+ let revisorChild = null;
740
+
741
+ function lerPortaDoPerfil(perfil) {
742
+ try {
743
+ const linhas = fs.readFileSync(path.join(perfil, 'DevToolsActivePort'), 'utf-8').split('\n');
744
+ const n = Number(linhas[0]);
745
+ return Number.isInteger(n) && n > 0 ? n : 0;
746
+ } catch { return 0; }
747
+ }
748
+
749
+ async function subirRevisor() {
750
+ if (PORTA_REVISOR) {
751
+ try {
752
+ const v = await httpGetJson(`http://127.0.0.1:${PORTA_REVISOR}/json/version`, 800);
753
+ if (v && v.webSocketDebuggerUrl) return true;
754
+ } catch {}
755
+ }
756
+
757
+ const chrome = findChrome();
758
+ if (!chrome) {
759
+ throw new Error('Sem navegador para revisar. O revisor abre a página num Chrome invisível '
760
+ + 'para ver o que quebra; instale Chrome, Edge, Brave ou Chromium, ou aponte o seu '
761
+ + 'com PRIMOCODE_CHROME=/caminho/do/navegador.');
762
+ }
763
+ perfilRevisor = fs.mkdtempSync(path.join(os.tmpdir(), 'primocode-revisor-'));
764
+
765
+ const args = [
766
+ // Zero: o Chrome escolhe uma porta livre e escreve qual no perfil.
767
+ `--remote-debugging-port=${process.env.PRIMOCODE_CDP_PORT_REVISOR || 0}`,
768
+ `--user-data-dir=${perfilRevisor}`,
769
+ // Sempre invisível: é o pedido, e é o que separa este do navegador da
770
+ // pessoa. Não há variável para ligar a janela aqui de propósito.
771
+ '--headless=new', '--disable-gpu', '--disable-dev-shm-usage',
772
+ '--hide-scrollbars', '--mute-audio',
773
+ '--no-first-run', '--no-default-browser-check',
774
+ '--disable-features=Translate',
775
+ 'about:blank',
776
+ ];
777
+ if (process.platform === 'linux' && process.getuid && process.getuid() === 0) {
778
+ args.splice(3, 0, '--no-sandbox');
779
+ }
780
+ revisorChild = spawn(chrome, args, { detached: true, stdio: 'ignore' });
781
+ revisorChild.on('error', () => {});
782
+ revisorChild.unref();
783
+
784
+ const prazo = Date.now() + 15000;
785
+ while (Date.now() < prazo) {
786
+ const porta = Number(process.env.PRIMOCODE_CDP_PORT_REVISOR) || lerPortaDoPerfil(perfilRevisor);
787
+ if (porta) {
788
+ try {
789
+ const v = await httpGetJson(`http://127.0.0.1:${porta}/json/version`, 500);
790
+ if (v && v.webSocketDebuggerUrl) { PORTA_REVISOR = porta; return true; }
791
+ } catch { /* ainda subindo */ }
792
+ }
793
+ await sleep(250);
794
+ }
795
+ throw new Error('o navegador do revisor subiu mas não abriu a porta de controle');
796
+ }
797
+
798
+ /* O que o navegador reclamou vira uma linha legível. O texto volta para outro
799
+ MODELO ler, então ele precisa dizer o problema e onde — "TypeError: x is not
800
+ a function (app.js:14)" conserta; "erro no console" não. */
801
+ function _falaDoErro(metodo, p) {
802
+ if (metodo === 'Runtime.exceptionThrown') {
803
+ const d = p.exceptionDetails || {};
804
+ const alvo = d.exception || {};
805
+ const onde = d.url ? ` (${String(d.url).split('/').pop()}:${(d.lineNumber || 0) + 1})` : '';
806
+ const texto = alvo.description || alvo.value || d.text || 'exceção sem descrição';
807
+ return { tipo: 'javascript', texto: String(texto).split('\n')[0] + onde };
808
+ }
809
+ if (metodo === 'Log.entryAdded') {
810
+ const e = p.entry || {};
811
+ if (e.level !== 'error') return null;
812
+ /* O NAVEGADOR PEDE ISTO SOZINHO, e a página não tem culpa. Sem esta
813
+ linha, TODA página sem favicon chegava ao revisor com um erro de
814
+ recurso — e um revisor que reprova o que está certo custa uma volta
815
+ inteira do pipeline, toda vez, para não mudar nada. Foi medido: a
816
+ página correta do teste era a única a reprovar. */
817
+ if (/\/favicon\.ico\b/.test(String(e.url || '')) || /\/\.well-known\//.test(String(e.url || ''))) {
818
+ return null;
819
+ }
820
+ const onde = e.url ? ` (${String(e.url).split('/').pop()})` : '';
821
+ return { tipo: e.source === 'network' ? 'recurso' : 'console', texto: String(e.text || '').slice(0, 220) + onde };
822
+ }
823
+ if (metodo === 'Runtime.consoleAPICalled') {
824
+ if (p.type !== 'error') return null;
825
+ const texto = (p.args || []).map((a) => a.description || a.value || a.type).join(' ');
826
+ return { tipo: 'console', texto: String(texto).slice(0, 220) };
827
+ }
828
+ return null;
829
+ }
830
+
831
+ /* A sonda roda DENTRO da página: é a única forma de saber se sobrou alguma
832
+ coisa na tela. Conta elemento com área de verdade — `display:none`,
833
+ `visibility:hidden` e opacidade zero não valem, e é assim que uma página
834
+ "cheia" de nós invisíveis deixa de passar por cheia. */
835
+ const SONDA = `(() => {
836
+ const corpo = document.body;
837
+ if (!corpo) return { titulo: document.title || '', texto: '', visiveis: 0, altura: 0 };
838
+ let visiveis = 0;
839
+ const todos = corpo.querySelectorAll('*');
840
+ for (const el of todos) {
841
+ const r = el.getBoundingClientRect();
842
+ if (r.width <= 4 || r.height <= 4) continue;
843
+ const s = getComputedStyle(el);
844
+ if (s.visibility === 'hidden' || s.display === 'none' || parseFloat(s.opacity) < 0.05) continue;
845
+ visiveis++;
846
+ }
847
+ return { titulo: document.title || '', texto: (corpo.innerText || '').trim().slice(0, 600),
848
+ visiveis, altura: corpo.scrollHeight, nos: todos.length };
849
+ })()`;
850
+
851
+ /**
852
+ * Abre uma página num navegador invisível e conta o que deu errado.
853
+ *
854
+ * @param {string} url endereço a abrir (http://… ou file://…)
855
+ * @param {object} op
856
+ * @param {number} op.assentar ms de espera depois do load, para o JS rodar
857
+ * @returns {Promise<{ok:boolean,url:string,titulo:string,erros:Array,vazio:boolean,texto:string,visiveis:number}>}
858
+ */
859
+ async function inspecionarEscondido(url, op = {}) {
860
+ await subirRevisor();
861
+ const aba = await comTeto(
862
+ httpPutJson(`http://127.0.0.1:${PORTA_REVISOR}/json/new?${encodeURIComponent('about:blank')}`, 5000),
863
+ 8000, 'a aba do revisor');
864
+ if (!aba || !aba.webSocketDebuggerUrl) throw new Error('o revisor não abriu uma aba');
865
+
866
+ const s = new CDPSession();
867
+ const erros = [];
868
+ s.onEvent = (metodo, p) => {
869
+ const linha = _falaDoErro(metodo, p);
870
+ // Teto: uma página em laço de erro produz milhares de linhas iguais, e
871
+ // o que volta daqui vai para dentro do contexto de outro modelo.
872
+ if (linha && erros.length < 25 && !erros.some((e) => e.texto === linha.texto)) erros.push(linha);
873
+ };
874
+ try {
875
+ await comTeto(s.attach(aba.webSocketDebuggerUrl), 12000, 'o canal do revisor');
876
+ await s.send('Page.enable');
877
+ await s.send('Runtime.enable');
878
+ await s.send('Log.enable');
879
+ await s.send('Page.navigate', { url });
880
+
881
+ const prazo = Date.now() + (op.prazo || 20000);
882
+ while (Date.now() < prazo) {
883
+ const r = await s.send('Runtime.evaluate', {
884
+ expression: 'document.readyState', returnByValue: true,
885
+ }).catch(() => null);
886
+ if (r && r.result && r.result.value === 'complete') break;
887
+ await sleep(200);
888
+ }
889
+ // Depois do load ainda roda o que espera DOMContentLoaded, o que está
890
+ // num módulo e o que pede a primeira rede. Sem esta pausa o revisor
891
+ // aprovaria a página um instante antes de ela quebrar.
892
+ await sleep(op.assentar || 1200);
893
+
894
+ const sonda = await s.send('Runtime.evaluate', {
895
+ expression: SONDA, returnByValue: true, awaitPromise: false,
896
+ }).catch(() => null);
897
+ const v = (sonda && sonda.result && sonda.result.value) || {};
898
+
899
+ return {
900
+ ok: true,
901
+ url,
902
+ titulo: v.titulo || '',
903
+ texto: v.texto || '',
904
+ visiveis: v.visiveis || 0,
905
+ // Três elementos com área e dez letras é o piso do que se pode
906
+ // chamar de página. Abaixo disso, o que a pessoa vê é uma tela
907
+ // branca — com ou sem erro no console.
908
+ vazio: (v.visiveis || 0) < 3 && (v.texto || '').length < 10,
909
+ erros,
910
+ };
911
+ } finally {
912
+ try { s.close(); } catch {}
913
+ try { await httpGetJson(`http://127.0.0.1:${PORTA_REVISOR}/json/close/${aba.id}`, 2000); } catch {}
914
+ }
915
+ }
916
+
917
+ /* O revisor é ferramenta de uma tarefa, não um processo que fica. Deixá-lo de
918
+ pé come memória até a pessoa desligar o computador, e ele não é o navegador
919
+ dela — ninguém sentiria falta nem saberia que existe para fechar. */
920
+ function fecharRevisor() {
921
+ if (revisorChild && revisorChild.pid) {
922
+ try { process.kill(-revisorChild.pid); } catch {
923
+ try { revisorChild.kill(); } catch {}
924
+ }
925
+ }
926
+ revisorChild = null;
927
+ PORTA_REVISOR = Number(process.env.PRIMOCODE_CDP_PORT_REVISOR || 0);
928
+ if (perfilRevisor) {
929
+ // O perfil é temporário: sem apagar, cada revisão deixa dezenas de MB
930
+ // em /tmp que ninguém sabe de quem são.
931
+ try { fs.rmSync(perfilRevisor, { recursive: true, force: true }); } catch { /* o SO limpa */ }
932
+ perfilRevisor = null;
933
+ }
934
+ return { ok: true };
935
+ }
936
+
647
937
  module.exports = {
648
938
  open, click, clickText, clickSelector, type, typeInSelector,
649
939
  key, screenshot, evaluate, waitFor, wait, scroll, close,
940
+ inspecionarEscondido, fecharRevisor,
650
941
  };
package/lib/catalogo.js CHANGED
@@ -93,8 +93,12 @@ const GRUPOS = [
93
93
  args: 'id da peça. pasta (opcional): nome da pasta. fps (opcional, padrão 30). renderizar: true só se o npm install já foi feito. '
94
94
  + 'Use SÓ quando o usuário pedir Remotion pelo nome, ou quando precisar de render determinístico, vídeo longo, codec/fps específico ou o projeto em React. '
95
95
  + 'O caminho normal continua sendo studio_criar + o botão de baixar, que sai em segundos sem instalar nada.' },
96
- { tool: 'studio_analisar', escreve: false, faz: 'ouve um vídeo já enviado e diz onde estão as pausas, para cortá-las',
97
- args: 'arquivo: a URL "/midia/..." do vídeo. pausa (opcional): segundos a partir dos quais conta como pausa (padrão 0.6). Devolve "trechos": vire cada um numa cena com {"tipo":"clipe","de":...,"ate":...}.' },
96
+ { tool: 'studio_analisar', escreve: false, faz: 'ouve um vídeo já enviado: encurta as pausas e tira a fala que a pessoa errou e refez',
97
+ args: 'arquivo: a URL "/midia/..." do vídeo. pausa (opcional): segundos a partir dos quais conta como pausa (padrão 0.6). '
98
+ + 'manter (opcional): quantos segundos de cada pausa ficam — sem isso ele decide sozinho (respiração fica quase inteira, pausa longa fica 0,7s). manter:0 corta seco. '
99
+ + 'repetida (opcional, padrão true): false desliga o corte de fala repetida. '
100
+ + 'Devolve "trechos": vire cada um numa cena com {"tipo":"clipe","de":...,"ate":...}. '
101
+ + 'Devolve também "repetidos": o que foi tirado por ter sido dito duas vezes — CONTE isso ao usuário, com os tempos, porque é fala dele que sumiu do vídeo.' },
98
102
  ],
99
103
  },
100
104
  {
@@ -143,6 +147,11 @@ const GRUPOS = [
143
147
  // Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
144
148
  { tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true,
145
149
  args: 'description: 3 a 5 palavras, aparece na tela do usuário. prompt: instrução completa e autossuficiente — o subagente não vê esta conversa.' },
150
+ { tool: 'construir_app', escreve: true, noLoop: true,
151
+ faz: 'constrói um app ou site inteiro com quatro papéis: engenheiro de prompt → protótipo → MVP → revisor que testa escondido, em laço até passar',
152
+ args: 'pedido: o que o usuário quer, com as palavras dele. pasta (opcional): onde construir — sem ela, uma pasta nova com o nome do pedido. rodadas (opcional, 1 a 4, padrão 2): quantas voltas de correção. '
153
+ + 'USE quando pedirem um app, um site, uma página ou uma ferramenta INTEIRA, do zero. NÃO use para mexer no que já existe, para um arquivo só, nem para conserto: são seis a oito chamadas de agente, '
154
+ + 'e gastá-las para trocar a cor de um botão come o dia do usuário. O revisor ABRE a página num navegador invisível e mede — página em branco e erro de JavaScript reprovam sozinhos, o que ele achar volta para o construtor.' },
146
155
  { tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true,
147
156
  args: 'summary (opcional): uma frase do que foi feito.' },
148
157
  ],
@@ -14,6 +14,27 @@ const path = require('path');
14
14
  const os = require('os');
15
15
  const { execSync, execFileSync } = require('child_process');
16
16
 
17
+ /* ── O MOTOR EXTERNO NÃO APARECE ────────────────────────────────────────
18
+ *
19
+ * "O Primo Code abre esses processos ocultamente (em segundo plano) e usa
20
+ * apenas o modelo dessas ferramentas. Toda a interface, comandos, agentes e
21
+ * experiência continuam sendo 100% do Primo Code."
22
+ *
23
+ * `execFileSync` sem `stdio` HERDA o stderr do pai — é o padrão do Node, e não
24
+ * parece nada até o dia em que o motor imprime um aviso de versão, de
25
+ * atualização ou de configuração. Aquilo cai no meio da tela do PrimoCode, com
26
+ * a cara e o vocabulário de outro produto, no meio de uma tarefa.
27
+ *
28
+ * Com o stderr CAPTURADO ele some da tela e ganha um segundo uso: `e.stderr` só
29
+ * existe quando é canalizado, e o tratamento de erro daqui (e o do
30
+ * codex-engine) já lia esse campo — estava lendo `null` desde sempre, e a
31
+ * mensagem de falha do motor saía sem o motivo que ele mesmo tinha escrito.
32
+ *
33
+ * `windowsHide` é a outra metade do "ocultamente": sem ele, cada chamada
34
+ * pisca um console preto na frente de quem usa Windows. */
35
+ const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
36
+
37
+
17
38
  const MARCA_INICIO = '<<<PRIMOCODE>>>';
18
39
  const MARCA_FIM = '<<<FIM>>>';
19
40
 
@@ -233,7 +254,8 @@ function escolherCaminho({ temClaude, temOrcaCli, orcaOk }) {
233
254
  let sessao = { handle: null, cursor: '0', bootstrapEnviado: false };
234
255
 
235
256
  function rodarOrca(args, opts = {}) {
236
- return execFileSync('orca', args, { encoding: 'utf-8', timeout: opts.timeout || 15000 });
257
+ return execFileSync('orca', args,
258
+ { encoding: 'utf-8', timeout: opts.timeout || 15000, ...SEM_APARECER });
237
259
  }
238
260
 
239
261
  let contadorId = 0;
@@ -548,7 +570,7 @@ async function requestActViaHeadless(payload, onEvent) {
548
570
  '--dangerously-skip-permissions',
549
571
  ...argsDoModelo(),
550
572
  ], { encoding: 'utf-8', timeout: 180000, input: prompt, maxBuffer: 32 * 1024 * 1024,
551
- env: chaves.ambienteDe('claude') });
573
+ env: chaves.ambienteDe('claude'), ...SEM_APARECER });
552
574
  const r = JSON.parse(saida);
553
575
  // A correção é uma segunda ida ao motor, com o mesmo protocolo e o
554
576
  // diagnóstico do que saiu errado. Barata: o prompt é curto, e evita que
@@ -561,6 +583,7 @@ async function requestActViaHeadless(payload, onEvent) {
561
583
  encoding: 'utf-8', timeout: 120000, maxBuffer: 32 * 1024 * 1024,
562
584
  env: chaves.ambienteDe('claude'),
563
585
  input: promptDeCorrecao(aviso),
586
+ ...SEM_APARECER,
564
587
  });
565
588
  return JSON.parse(s).result || '';
566
589
  };
@@ -32,6 +32,13 @@
32
32
 
33
33
  const { execSync, execFileSync } = require('child_process');
34
34
 
35
+ /* O MOTOR EXTERNO NÃO APARECE — a mesma regra do claude-engine, e o mesmo
36
+ comentário longo mora lá. Em resumo: `execFileSync` sem `stdio` HERDA o
37
+ stderr, e um aviso do Codex cairia no meio da tela do PrimoCode com a cara de
38
+ outro produto. Capturado, ele some da tela e `e.stderr` passa a existir — o
39
+ `catch` daqui já lia esse campo e estava lendo `null` desde sempre. */
40
+ const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
41
+
35
42
  const claude = require('./claude-engine');
36
43
  const chaves = require('./chaves');
37
44
 
@@ -78,7 +85,7 @@ let _ajuda = null;
78
85
  function ajudaDoExec(executor) {
79
86
  if (_ajuda !== null) return _ajuda;
80
87
  const rodar = executor || ((args) => execFileSync(CODEX, args,
81
- { encoding: 'utf-8', timeout: 20000, stdio: ['ignore', 'pipe', 'ignore'] }));
88
+ { encoding: 'utf-8', timeout: 20000, ...SEM_APARECER }));
82
89
  try { _ajuda = String(rodar(['exec', '--help']) || ''); }
83
90
  catch { _ajuda = ''; } // sem ajuda: segue com o mínimo
84
91
  return _ajuda;
@@ -132,7 +139,7 @@ function chamarCodex(prompt, { executor, avisar = () => {} } = {}) {
132
139
  // sido "configurado".
133
140
  const rodar = executor || ((args, entrada) => execFileSync(CODEX, args, {
134
141
  encoding: 'utf-8', timeout: TEMPO, input: entrada, maxBuffer: BUFFER,
135
- env: chaves.ambienteDe('codex'),
142
+ env: chaves.ambienteDe('codex'), ...SEM_APARECER,
136
143
  }));
137
144
  try {
138
145
  return rodar(argsDoExec(), prompt);