primocode 8.34.1 → 8.36.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/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);
package/lib/icone.js CHANGED
@@ -82,56 +82,72 @@ function caixa(t, x0, y0, l, a, r, cor) {
82
82
 
83
83
  // ── o mascote ────────────────────────────────────────────────────────────
84
84
 
85
- /* O Primo, o mesmo do terminal: cabeça de topo arredondado, ombros largos,
86
- * dois pezinhos. Lá ele é feito de glifos de bloco:
85
+ /* O DESENHO VEM DE UM LUGAR SÓ.
87
86
  *
88
- * ▐▛███▜▌ a cabeça
89
- * ▝▜█████▛▘ o corpo, mais largo
90
- * ▘▘ ▝▝ os pés
87
+ * Antes esta função desenhava um bicho à mão — cabeça arredondada, ombros,
88
+ * dois pés, olhos no negativo — e o app e o site desenhavam OUTRO, a matriz de
89
+ * pixels do logo que o dono mandou. Dois desenhos com o mesmo nome: o ícone no
90
+ * Dock não era o bicho que aparecia dentro do programa.
91
91
  *
92
- * Aqui são as mesmas três peças em proporção, e não uma transliteração dos
93
- * glifos: meio bloco de terminal vira escada num ícone de 512px. O que faz
94
- * reconhecer é a SILHUETA — topo redondo, ombro saliente, dois pés —, e ela é
95
- * a mesma.
96
- */
92
+ * Agora os dois leem a mesma `MATRIZ` do `app/primo-marca.js`. Mudar o logo é
93
+ * mudar onze linhas de texto, e o Dock, a janela e o site mudam juntos.
94
+ *
95
+ * (Aquele arquivo é escrito para o navegador, mas se pendura no `globalThis`
96
+ * quando não há `window` — então dá para carregá-lo aqui sem uma segunda
97
+ * versão dele.) */
98
+ require('../app/primo-marca.js');
99
+ const MARCA = (typeof globalThis !== 'undefined' && globalThis.PrimoMarca) || null;
100
+
101
+ /** #5ec8ef → [94, 200, 239] */
102
+ function doHex(h) {
103
+ const n = parseInt(String(h).replace('#', ''), 16);
104
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
105
+ }
106
+
97
107
  function desenharPrimo(n) {
98
108
  const t = tela(n);
99
- const u = n / 32; // a grade do desenho, em pixels
100
109
 
101
- // A base: o quadrado escuro do app, com o mesmo arredondado do ícone de
102
- // sistema (22% do lado é o raio que o macOS e o Windows usam hoje).
110
+ // A base: o quadrado escuro do app, com o mesmo arredondado que o macOS e
111
+ // o Windows usam hoje (22% do lado). É o formato de azulejo do estilo
112
+ // pedido — glifo chapado, centrado, sobre cor sólida.
103
113
  caixa(t, 0, 0, n, n, n * 0.22, FUNDO);
104
114
 
105
- // Um brilho fraco atrás do bicho — o mesmo halo do fundo do app.
115
+ // Um brilho fraco atrás do bicho, para o azulejo não ficar chapado.
106
116
  for (let y = 0; y < n; y++) {
107
117
  for (let x = 0; x < n; x++) {
108
- const dx = (x - n / 2) / (n * 0.6);
109
- const dy = (y - n * 0.34) / (n * 0.6);
118
+ const dx = (x - n / 2) / (n * 0.62);
119
+ const dy = (y - n * 0.42) / (n * 0.62);
110
120
  const d = Math.sqrt(dx * dx + dy * dy);
111
- if (d < 1) ponto(t, x, y, AZUL, 0.11 * (1 - d) * (1 - d));
121
+ if (d < 1) ponto(t, x, y, AZUL, 0.10 * (1 - d) * (1 - d));
112
122
  }
113
123
  }
114
124
 
115
- // Cabeça: topo arredondado, ombros retos embaixo (raio só em cima é o que
116
- // dá a silhueta do ▐▛███▜▌ — redondo por cima, quadrado por baixo).
117
- caixa(t, 9 * u, 7 * u, 14 * u, 11 * u, 3.4 * u, AZUL);
118
- caixa(t, 9 * u, 14 * u, 14 * u, 4 * u, 0.6 * u, AZUL);
119
-
120
- // Corpo: mais largo que a cabeça, que é o ombro marcado do desenho.
121
- caixa(t, 6 * u, 17.6 * u, 20 * u, 7.6 * u, 2.2 * u, AZUL);
125
+ if (!MARCA) return t; // sem a matriz, fica o azulejo
122
126
 
123
- // Os dois pés.
124
- caixa(t, 10 * u, 24.6 * u, 4.4 * u, 2.6 * u, 1 * u, AZUL_FUNDO);
125
- caixa(t, 17.6 * u, 24.6 * u, 4.4 * u, 2.6 * u, 1 * u, AZUL_FUNDO);
127
+ const linhas = MARCA.MATRIZ;
128
+ const colunas = linhas[0].length;
129
+ const corpo = doHex(MARCA.CORPO);
130
+ const detalhe = doHex(MARCA.DETALHE);
126
131
 
127
- // Os olhos, no negativo — é o que faz o bloco virar bicho.
128
- caixa(t, 12.4 * u, 10.6 * u, 2.6 * u, 3.2 * u, 1.1 * u, FUNDO);
129
- caixa(t, 17 * u, 10.6 * u, 2.6 * u, 3.2 * u, 1.1 * u, FUNDO);
132
+ /* O bicho ocupa 62% do azulejo. Mais que isso ele encosta na borda
133
+ * arredondada e o ícone fica apertado; menos, e some no tamanho de 16px da
134
+ * barra de tarefas — que é onde ele mais aparece. */
135
+ const lado = Math.floor((n * 0.62) / colunas);
136
+ const largura = lado * colunas;
137
+ const altura = lado * linhas.length;
138
+ const x0 = Math.round((n - largura) / 2);
139
+ const y0 = Math.round((n - altura) / 2);
130
140
 
131
- // A luz de cima: uma faixa clara na testa e no ombro. Sem ela o bicho é
132
- // uma silhueta chapada — é o mesmo bisel do "vidro" do estúdio.
133
- caixa(t, 10.4 * u, 8.2 * u, 11.2 * u, 1.4 * u, 0.7 * u, BRILHO);
134
- caixa(t, 7.6 * u, 18.6 * u, 16.8 * u, 1.2 * u, 0.6 * u, BRILHO);
141
+ for (let f = 0; f < linhas.length; f++) {
142
+ for (let cIdx = 0; cIdx < colunas; cIdx++) {
143
+ const ch = linhas[f][cIdx];
144
+ if (ch !== 'C' && ch !== 'D') continue;
145
+ // Sem raio: pixel art com canto redondo deixa de ser pixel art, e
146
+ // vira um monte de bolinhas quando o quadrado é pequeno.
147
+ caixa(t, x0 + cIdx * lado, y0 + f * lado, lado, lado, 0,
148
+ ch === 'C' ? corpo : detalhe);
149
+ }
150
+ }
135
151
 
136
152
  return t;
137
153
  }
@@ -115,10 +115,33 @@ function paraEnviar(memoria, history) {
115
115
  }
116
116
 
117
117
  /** Quanto este jeito economiza — para o /status e para o teste provarem. */
118
+ /* ── O QUANTO DA CONVERSA JÁ ESTÁ NA MESA ────────────────────────────────
119
+ * A barra do terminal mostra isso em porcentagem, e a porcentagem precisa de
120
+ * um teto para ser porcentagem DE alguma coisa.
121
+ *
122
+ * O teto de verdade é do servidor e muda com o modelo e com o minuto (ele
123
+ * divide o orçamento entre prompt, ferramentas e conversa a cada pedido). O
124
+ * CLI não vê esse número, e fingir que vê seria pior que não mostrar: a barra
125
+ * diria 20% no exato momento em que o pedido não coubesse.
126
+ *
127
+ * Então o teto aqui é declarado e é DA CONVERSA: quanto de histórico vale a
128
+ * pena carregar antes de resumir. É uma régua honesta — quem chega perto de
129
+ * 100% está mesmo com a conversa pesada, e `/compact` resolve. */
130
+ const TETO_CONVERSA = 4000;
131
+
132
+ function usoDoContexto(memoria, history) {
133
+ const tokens = tokensDe(paraEnviar(memoria, history));
134
+ return {
135
+ tokens,
136
+ teto: TETO_CONVERSA,
137
+ pct: Math.min(999, Math.round((tokens / TETO_CONVERSA) * 100)),
138
+ };
139
+ }
140
+
118
141
  function economia(memoria, history) {
119
142
  const cheio = tokensDe((history || []).slice(-20));
120
143
  const enxuto = tokensDe(paraEnviar(memoria, history));
121
144
  return { cheio, enxuto, poupado: Math.max(0, cheio - enxuto) };
122
145
  }
123
146
 
124
- module.exports = { nova, pendentes, precisaResumir, atualizar, paraEnviar, economia, tokensDe, inicioSeguro, CRUAS, LIMITE_TOKENS };
147
+ module.exports = { nova, pendentes, usoDoContexto, TETO_CONVERSA, precisaResumir, atualizar, paraEnviar, economia, tokensDe, inicioSeguro, CRUAS, LIMITE_TOKENS };