primocode 10.19.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSES/opencode-MIT.txt +24 -0
  2. package/LICENSES/pptxgenjs-MIT.txt +31 -0
  3. package/README.md +16 -3
  4. package/agentes/acessibilidade.md +10 -0
  5. package/agentes/ambiente.md +10 -0
  6. package/agentes/api.md +10 -0
  7. package/agentes/backend.md +10 -0
  8. package/agentes/copy.md +10 -0
  9. package/agentes/css-layout.md +10 -0
  10. package/agentes/dados.md +10 -0
  11. package/agentes/depuracao.md +10 -0
  12. package/agentes/design.md +10 -0
  13. package/agentes/docs.md +10 -0
  14. package/agentes/frontend.md +10 -0
  15. package/agentes/geral/capacidades/arquivos.json +132 -0
  16. package/agentes/geral/capacidades/codigo.json +132 -0
  17. package/agentes/geral/capacidades/dados.json +126 -0
  18. package/agentes/geral/capacidades/depuracao.json +126 -0
  19. package/agentes/geral/capacidades/escrita.json +126 -0
  20. package/agentes/geral/capacidades/git.json +126 -0
  21. package/agentes/geral/capacidades/pesquisa.json +126 -0
  22. package/agentes/geral/capacidades/seguranca.json +132 -0
  23. package/agentes/geral/capacidades/shell.json +132 -0
  24. package/agentes/geral/capacidades/web.json +126 -0
  25. package/agentes/git-pr.md +10 -0
  26. package/agentes/indice.md +86 -0
  27. package/agentes/nextjs.md +10 -0
  28. package/agentes/node.md +10 -0
  29. package/agentes/performance.md +10 -0
  30. package/agentes/pesquisa.md +10 -0
  31. package/agentes/python.md +10 -0
  32. package/agentes/react.md +10 -0
  33. package/agentes/refatoracao.md +10 -0
  34. package/agentes/revisor.md +10 -0
  35. package/agentes/seguranca.md +10 -0
  36. package/agentes/sql.md +10 -0
  37. package/agentes/testes.md +10 -0
  38. package/agentes/typescript.md +10 -0
  39. package/agentes/ux.md +10 -0
  40. package/bin/primocode.js +65 -8
  41. package/editor/helpers/camera_viva.py +28 -2
  42. package/lib/act.js +79 -10
  43. package/lib/agentes/index.js +146 -0
  44. package/lib/agentes/nucleo.js +18 -0
  45. package/lib/catalogo.js +12 -2
  46. package/lib/cli/review.js +64 -0
  47. package/lib/cli/subcomandos.js +34 -0
  48. package/lib/cli/tentativas.js +107 -0
  49. package/lib/cli/util.js +51 -0
  50. package/lib/comandos-extra.js +4 -2
  51. package/lib/editor-pro-maquina.js +165 -0
  52. package/lib/editor-pro.js +10 -7
  53. package/lib/entregaveis/cli.js +26 -0
  54. package/lib/entregaveis/especialistas.js +50 -0
  55. package/lib/entregaveis/nucleo.js +111 -0
  56. package/lib/entregaveis/slides.js +221 -0
  57. package/lib/estudio/materiais.js +172 -0
  58. package/lib/fundo.js +34 -1
  59. package/lib/kits.js +41 -2
  60. package/lib/nomes.js +6 -0
  61. package/lib/placar.js +173 -0
  62. package/lib/plugin/autoteste.js +99 -0
  63. package/lib/plugin/edicao-visual.js +133 -0
  64. package/lib/plugin/pesquisar.js +92 -0
  65. package/lib/plugin/preview-cli.js +38 -0
  66. package/lib/plugin/preview.js +89 -0
  67. package/lib/plugin/tunel.js +27 -0
  68. package/lib/review.js +134 -0
  69. package/lib/session/compactacao.js +124 -0
  70. package/lib/statusline.js +52 -0
  71. package/lib/tool/diagnostico.js +100 -0
  72. package/lib/tool/perguntar.js +60 -0
  73. package/lib/tool/reparar-entrada.js +66 -0
  74. package/lib/tool/web.js +102 -0
  75. package/lib/tools.js +20 -1
  76. package/lib/ui.js +1 -1
  77. package/materiais/saas-linear/base.ts +84 -0
  78. package/materiais/saas-linear/camera/Camera.tsx +38 -0
  79. package/materiais/saas-linear/fontes/OFL-Inter.txt +93 -0
  80. package/materiais/saas-linear/fontes/OFL-Newsreader.txt +93 -0
  81. package/materiais/saas-linear/fontes/inter-400.woff2 +0 -0
  82. package/materiais/saas-linear/fontes/inter-600.woff2 +0 -0
  83. package/materiais/saas-linear/fontes/newsreader-400.woff2 +0 -0
  84. package/materiais/saas-linear/formas/Agenda.tsx +46 -0
  85. package/materiais/saas-linear/formas/CaixaPrompt.tsx +41 -0
  86. package/materiais/saas-linear/formas/CartoesIntegracao.tsx +57 -0
  87. package/materiais/saas-linear/formas/Cursor.tsx +40 -0
  88. package/materiais/saas-linear/formas/Menu.tsx +46 -0
  89. package/materiais/saas-linear/formas/Monitor.tsx +28 -0
  90. package/materiais/saas-linear/paleta.json +18 -0
  91. package/materiais/saas-linear/pecas.json +590 -0
  92. package/materiais/saas-linear/regras.md +40 -0
  93. package/materiais/saas-linear/sons/Som.tsx +39 -0
  94. package/materiais/saas-linear/sons/assinatura.mp3 +0 -0
  95. package/materiais/saas-linear/sons/clique.mp3 +0 -0
  96. package/materiais/saas-linear/sons/impacto.mp3 +0 -0
  97. package/materiais/saas-linear/sons/pop-cartao.mp3 +0 -0
  98. package/materiais/saas-linear/sons/pop-palavra.mp3 +0 -0
  99. package/materiais/saas-linear/sons/subida.mp3 +0 -0
  100. package/materiais/saas-linear/sons/swoosh.mp3 +0 -0
  101. package/materiais/saas-linear/sons/trilha.mp3 +0 -0
  102. package/materiais/saas-linear/sons/whoosh-entra.mp3 +0 -0
  103. package/materiais/saas-linear/sons/whoosh-sai.mp3 +0 -0
  104. package/materiais/saas-linear/texturas/Fundo.tsx +18 -0
  105. package/materiais/saas-linear/texturas/Particulas.tsx +22 -0
  106. package/materiais/saas-linear/tipografia/FraseSerifa.tsx +25 -0
  107. package/materiais/saas-linear/tipografia/Marca.tsx +20 -0
  108. package/materiais/saas-linear/tipografia/MarcaFinal.tsx +22 -0
  109. package/materiais/saas-linear/tipografia/Saudacao.tsx +27 -0
  110. package/materiais/saas-linear/transicoes/Cena.tsx +38 -0
  111. package/materiais/saas-linear/transicoes/CorteImpacto.tsx +16 -0
  112. package/package.json +3 -1
  113. package/skills/deck-conferido/SKILL.md +29 -0
  114. package/vendor/pptxgenjs/carregar.js +12 -0
  115. package/vendor/pptxgenjs/jszip.min.js +13 -0
  116. package/vendor/pptxgenjs/pptxgen.cjs.js +7370 -0
package/lib/kits.js CHANGED
@@ -48,6 +48,7 @@ const crypto = require('crypto');
48
48
  const { spawn, spawnSync } = require('child_process');
49
49
 
50
50
  const RAIZ = path.join(__dirname, '..', 'kits');
51
+ const MATERIAIS = require('./estudio/materiais.js');
51
52
  const ESTUDIO = path.join(RAIZ, 'estudio');
52
53
  const FAMILIAS = ['SaaS e tecnologia', 'Tipografia em movimento', 'Retrô', 'Feito à mão', '3D e abstrato', 'Redes sociais', 'Marca e evento'];
53
54
  const REGRAS = ['tipografia.md', 'cor.md', 'movimento.md', 'som.md', 'ritmo.md', 'area-segura.md'];
@@ -135,7 +136,7 @@ function achar(nome, op = {}) {
135
136
 
136
137
  // ── o menu pra IA ────────────────────────────────────────────────────────
137
138
 
138
- const linhaDoMenu = (k) => `- ${k.slug} · ${k.nome.split(' · ')[0]}${k.origem === 'seu' ? ' (seu)' : ''}: ${(k.quando.length ? k.quando.slice(0, 2) : [k.descricao]).join('; ').replace(/\s+/g, ' ').replace(/"/g, '')}`.slice(0, 92);
139
+ const linhaDoMenu = (k) => `- ${k.slug} · ${k.nome.split(' · ')[0]}${k.origem === 'seu' ? ' (seu)' : ''}${MATERIAIS.migrado(k.slug) ? ' [peças soltas]' : ''}: ${(k.quando.length ? k.quando.slice(0, 2) : [k.descricao]).join('; ').replace(/\s+/g, ' ').replace(/"/g, '')}`.slice(0, 92);
139
140
 
140
141
  /** "estilo apple", "estilo papel recortado": a palavra depois de "estilo" que é o slug ou o nome de um kit. Só sugere. */
141
142
  function kitCitado(texto, todos) {
@@ -508,6 +509,7 @@ async function cmdMenu(op) {
508
509
  function cmdVer(slug, parte, qual, op) {
509
510
  const k = achar(slug, op);
510
511
  if (!k) return naoExiste(slug, op);
512
+ if (MATERIAIS.migrado(k.slug)) return { ok: true, texto: MATERIAIS.promptDoEstilo(k.slug) };
511
513
  return { ok: true, texto: ver(k, PARTES.includes(parte) ? parte : 'elementos', qual) };
512
514
  }
513
515
 
@@ -524,6 +526,7 @@ async function cmdPlano(a, op) {
524
526
  if (!a.kit) return { ok: false, texto: 'Falta --kit <slug>. Veja: primocode kit menu' };
525
527
  const k = achar(a.kit, op);
526
528
  if (!k) return naoExiste(a.kit, op);
529
+ if (MATERIAIS.migrado(k.slug)) return { ok: false, texto: `O estilo ${k.slug} é de peças soltas: não tem esqueleto nem plano pronto. Veja: primocode kit pecas ${k.slug}` };
527
530
  const nome = String(a.nome || '').trim();
528
531
  if (!nome) return { ok: false, texto: 'Falta --nome "<nome do vídeo>": vai no cartão que a pessoa confirma e no arquivo.' };
529
532
  const roteiro = lerTextoOuArquivo(a.roteiro, pasta).trim();
@@ -582,6 +585,7 @@ function cmdMontar(a, op) {
582
585
  if (!a.kit) return { ok: false, texto: 'Falta --kit <slug>. Veja: primocode kit menu' };
583
586
  const k = achar(a.kit, op);
584
587
  if (!k) return naoExiste(a.kit, op);
588
+ if (MATERIAIS.migrado(k.slug)) return { ok: false, texto: `O estilo ${k.slug} é de peças soltas: não tem esqueleto nem plano pronto. Veja: primocode kit pecas ${k.slug}` };
585
589
  const produto = String(a.produto || '').trim();
586
590
  if (!produto) return { ok: false, texto: 'Falta --produto "<o que o vídeo mostra: app de celular, curso online, garrafa térmica…>": é o que você vai montar em cena (nunca o produto da demo do kit).' };
587
591
  const arquivos = [...lista(a.arquivos), ...lista(a.logo), ...lista(a.fotos)];
@@ -938,6 +942,38 @@ function palavras(linha) {
938
942
  return out;
939
943
  }
940
944
 
945
+ // ── peças soltas (lib/estudio/materiais.js): a IA lista, lê e escreve a cena do zero ──
946
+
947
+ async function cmdPecas(a, op) {
948
+ const [estilo, sub, alvo] = a._;
949
+ if (!estilo) return { ok: true, texto: `Estilos com peças soltas: ${MATERIAIS.estilos().join(', ') || 'nenhum'}. Uso: primocode kit pecas <estilo> [ver <peça> | regras | escrever <nome> --arquivo <cena.tsx> | conferir <cena.tsx> | renderizar <nome>]` };
950
+ if (!MATERIAIS.migrado(estilo)) return { ok: false, texto: `O estilo "${estilo}" ainda não tem peças soltas (migrados: ${MATERIAIS.estilos().join(', ') || 'nenhum'}).` };
951
+ const pasta = op.projectDir || process.cwd();
952
+ const lerArq = (f) => fs.readFileSync(path.resolve(pasta, String(f)), 'utf8');
953
+ try {
954
+ switch (sub) {
955
+ case undefined: case 'listar': return MATERIAIS.listar(estilo);
956
+ case 'ver': return alvo ? MATERIAIS.lerPeca(estilo, alvo) : { ok: false, texto: 'Uso: primocode kit pecas <estilo> ver <peça>' };
957
+ case 'regras': return MATERIAIS.regras(estilo);
958
+ case 'escrever': return alvo && a.arquivo ? MATERIAIS.escreverCena(estilo, pasta, alvo, lerArq(a.arquivo)) : { ok: false, texto: 'Uso: primocode kit pecas <estilo> escrever <nome> --arquivo <cena.tsx>' };
959
+ case 'conferir': { const c = MATERIAIS.conferirCena(estilo, lerArq(alvo || a.arquivo)); return { ok: c.ok, texto: c.ok ? `ok ${JSON.stringify(c.medidas)}` : c.erros.join('\n') }; }
960
+ case 'renderizar': {
961
+ const arq = path.join(pasta, 'motion', `${alvo}.tsx`);
962
+ if (!alvo || !fs.existsSync(arq)) return { ok: false, texto: 'Grave a cena antes: … escrever <nome> --arquivo <cena.tsx>' };
963
+ const est = garantirEstudio({ home: op.home, aoFalar: op.aoFalar, env: op.env || process.env });
964
+ if (!est.ok) return { ok: false, texto: est.error };
965
+ const dir = path.join(pasta, 'motion', `.render-${alvo}`);
966
+ MATERIAIS.prepararRender(estilo, dir, arq, { duracao: a.quadros ? Number(a.quadros) : undefined });
967
+ ligarModulos(dir, est.modulos);
968
+ const saida = path.resolve(pasta, String(a.saida || path.join('motion', `${alvo}.mp4`)));
969
+ const r = await rodarRender(dir, saida, { escala: Number(a.escala) || 1, aoFalar: op.aoFalar || (() => {}) });
970
+ return r.ok ? { ok: true, texto: `Vídeo: ${r.video} (${r.segundos}s). Quadros pra conferir: ${(r.quadros || []).join(', ')}`, video: r.video } : { ok: false, texto: `O render falhou: ${r.error}` };
971
+ }
972
+ default: return { ok: false, texto: `Subcomando "${sub}" desconhecido. Use listar, ver, regras, escrever, conferir ou renderizar.` };
973
+ }
974
+ } catch (e) { return { ok: false, texto: `pecas: ${e.message}` }; }
975
+ }
976
+
941
977
  function opcoes(args) {
942
978
  const a = { _: [] };
943
979
  for (let i = 0; i < args.length; i++) {
@@ -958,6 +994,8 @@ const AJUDA = `primocode kit — os kits de estilo de motion
958
994
  o esqueleto do vídeo pela receita do kit (motion/plano.json) e o formulário da batida 1
959
995
  batida <n|geral> [--camadas '<json>' | --arquivo <json>]
960
996
  sem JSON: o formulário da batida; com: confere, conserta e grava (devolve a próxima)
997
+ pecas <estilo> [ver <peça>|regras|escrever <nome> --arquivo <tsx>|conferir <tsx>|renderizar <nome>]
998
+ estilos de peças soltas: a IA compõe a cena do zero (sem esqueleto pronto)
961
999
  plano --kit <slug> --nome "<nome>" --roteiro <md> [--plano <json>] [--fala "<texto>"] [--voz <voz>]
962
1000
  o cartão pra pessoa confirmar (roteiro + nome + estilo), com o plano montado
963
1001
  fazer [--escala 0.5] depois do sim: narração + render
@@ -975,6 +1013,7 @@ async function main(argv, op = {}) {
975
1013
  case 'menu': return await cmdMenu(op);
976
1014
  case 'ver': case 'skill': return a._[0] ? cmdVer(a._[0], a._[1], a._[2], op) : { ok: false, texto: 'Uso: primocode kit ver <slug>' };
977
1015
  case 'referencia': case 'referencias': return await cmdReferencia(a, op);
1016
+ case 'pecas': case 'materiais': return await cmdPecas(a, op);
978
1017
  case 'montar': return cmdMontar(a, op);
979
1018
  case 'batida': return cmdBatida(a, op);
980
1019
  case 'plano': return await cmdPlano(a, op);
@@ -1040,6 +1079,6 @@ const continuaVideo = (texto) => pareceMotion(texto) || CONTINUA.test(semAcento(
1040
1079
  module.exports = {
1041
1080
  batidasFaltando, continuaVideo,
1042
1081
  RAIZ, FAMILIAS, casa, listar, achar, lerFrontmatter, menuParaIA, cabecaDoPedido, pareceMotion, ver, tiposDe,
1043
- validarPlano, checar, esticar, limparPlano, palavras, opcoes, entradaDoPlano, galeriaHtml, listaParaPessoa, previewPronto,
1082
+ cmdPecas, validarPlano, checar, esticar, limparPlano, palavras, opcoes, entradaDoPlano, galeriaHtml, listaParaPessoa, previewPronto,
1044
1083
  novoTurno, turnoAtual, main, doShell, garantirEstudio, conferirQuadros,
1045
1084
  };
package/lib/nomes.js CHANGED
@@ -81,6 +81,9 @@ const COMANDOS = {
81
81
  skills: { grupo: 'jeito', antigos: ['marketplace', 'market', 'install-skill', 'installskill'] },
82
82
  commands: { grupo: 'jeito', antigos: ['comandos'] },
83
83
  schedule: { grupo: 'jeito', antigos: ['agendar', 'agenda'] },
84
+ 'testar-app': { grupo: 'rodar', antigos: [] },
85
+ pesquisar: { grupo: 'entender', antigos: [] },
86
+ placar: { grupo: 'entender', antigos: [] },
84
87
  secrets: { grupo: 'jeito', antigos: ['segredos'] },
85
88
  team: { grupo: 'jeito', antigos: ['time', 'equipe'] },
86
89
  // 8. Vídeo e marca
@@ -153,6 +156,9 @@ const QUANDO = {
153
156
  config: 'quer ver as preferências',
154
157
  keys: 'quer saber as teclas que ajudam',
155
158
  skills: 'quer ver ou ligar o que ele sabe fazer (`/skills store` abre a loja, `/skills add` instala)',
159
+ 'testar-app': 'quer que o app web se teste sozinho: abre, clica nos fluxos principais e conserta o que der erro',
160
+ placar: 'quer ver qual modelo grátis acerta mais em cada tipo de tarefa (`/placar`; `/placar zerar` apaga)',
161
+ pesquisar: 'quer um relatório com fontes sobre um tema (`/pesquisar <tema>` grava em pesquisas/)',
156
162
  schedule: 'quer que um pedido rode sozinho num dia e hora (`/schedule "toda segunda às 9h" <pedido>`; sem nada, lista)',
157
163
  secrets: 'quer ver as chaves que o projeto usa ou guardar uma (`/secrets add NOME`)',
158
164
  commands: 'quer criar ou ver os comandos dele',
package/lib/placar.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * placar.js — o PrimoCode aprende, por pessoa, qual modelo grátis acerta mais em cada tipo de tarefa.
3
+ *
4
+ * Tudo local (~/.primocode/placar.json): nada sai da máquina. Cada tarefa vira um TIPO (editar código,
5
+ * corrigir erro, app novo, design/CSS, pesquisa, escrita, vídeo/motion, documentos/slides). Cada
6
+ * desfecho vira um ponto pra (tipo, modelo): ganhou ou perdeu. O modelo é escolhido pela média
7
+ * Beta(1+ganhos, 1+perdas); só vale depois de MIN_AMOSTRAS; e em ~10% das vezes explora outro,
8
+ * pra o placar não travar no primeiro que acertou.
9
+ *
10
+ * Quem fixou modelo (/model) nunca é mexido. O servidor recebe a escolha como DICA (`preferido`)
11
+ * e só a honra se o modelo estiver na corrente do apelido; sem dica ou sem ele, nada muda.
12
+ *
13
+ * Sinais de ganho: testes passaram, diagnóstico limpo depois da edição, sem /undo logo depois,
14
+ * tarefa concluída. Perda: o contrário (teste falhando no fim, erro de diagnóstico sobrando,
15
+ * parou sem concluir, /undo).
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+
24
+ const TIPOS = {
25
+ 'editar-codigo': 'editar código',
26
+ 'corrigir-erro': 'corrigir erro',
27
+ 'app-novo': 'app novo',
28
+ 'design-css': 'design / CSS',
29
+ 'pesquisa': 'pesquisa',
30
+ 'escrita': 'escrita',
31
+ 'video-motion': 'vídeo / motion',
32
+ 'documentos': 'documentos / slides',
33
+ };
34
+ const MIN_AMOSTRAS = 5;
35
+ const EXPLORACAO = 0.10;
36
+
37
+ // Ordem importa: o mais específico primeiro.
38
+ const REGRAS = [
39
+ ['video-motion', /\b(v[ií]deo|motion|anima[çc][ãa]o|animar|reel|legenda|edi[çc][ãa]o de v|lettering|trilha|cena)\b/i],
40
+ ['documentos', /\b(slides?|apresenta[çc][ãa]o|pptx|docx|pdf|planilha|xlsx|documento|proposta|relat[óo]rio em)\b/i],
41
+ ['corrigir-erro', /\b(bug|erro|quebr\w+|n[ãa]o funciona|falh\w+|corrig\w+|conserta\w*|consert\w+|crash|exception|stack ?trace|travando|n[ãa]o abre)\b/i],
42
+ ['design-css', /\b(css|design|layout|visual|cor(es)?|fonte|tipografia|responsiv\w+|espa[çc]amento|bonit\w+|tema|dark mode|tailwind|estilo)\b/i],
43
+ ['app-novo', /\b(cri(e|ar|a) (um|uma|o|a) (app|aplicativo|site|landing|p[áa]gina|jogo|sistema|api)|do zero|novo (app|projeto|site)|landing page|construa|construir)\b/i],
44
+ ['pesquisa', /\b(pesquis\w+|compar\w+|qual (a )?(melhor|diferen[çc]a)|o que [ée]|como funciona|explique|explica|investig\w+|estude|levantamento)\b/i],
45
+ ['escrita', /\b(escrev\w+|texto|post|e-?mail|copy|roteiro|artigo|resum\w+|traduz\w*|reescrev\w+|legenda de post)\b/i],
46
+ ];
47
+
48
+ function classificar(texto) {
49
+ const t = String(texto || '').slice(0, 2000);
50
+ for (const [tipo, re] of REGRAS) if (re.test(t)) return tipo;
51
+ return 'editar-codigo';
52
+ }
53
+
54
+ const idCurto = (m) => String(m || '').replace(/^[a-z0-9]+:/i, ''); // tira o `gemini:`: o servidor conta o id limpo
55
+
56
+ // ── a matemática ─────────────────────────────────────────────────────
57
+ /** Média da Beta(1+ganhos, 1+perdas): começa em 50% e anda com os dados. */
58
+ function pontuacao(ganhos, perdas) { return (1 + ganhos) / (2 + ganhos + perdas); }
59
+
60
+ function amostras(c) { return (c && (c.ganhos || 0) + (c.perdas || 0)) || 0; }
61
+
62
+ /**
63
+ * Escolhe o modelo do tipo entre os disponíveis. `rand` é injetável (teste).
64
+ * Volta { modelo, motivo } ou null (sem dica: o servidor decide como sempre).
65
+ */
66
+ function escolher(dados, tipo, disponiveis, { rand = Math.random, minimo = MIN_AMOSTRAS, exploracao = EXPLORACAO } = {}) {
67
+ const lista = (disponiveis || []).filter(Boolean);
68
+ if (!lista.length) return null;
69
+ const celulas = (dados && dados.tipos && dados.tipos[tipo]) || {};
70
+ const conta = (m) => celulas[idCurto(m)] || { ganhos: 0, perdas: 0 };
71
+ const confiaveis = lista.filter((m) => amostras(conta(m)) >= minimo);
72
+ if (!confiaveis.length) return null; // ainda não sabe: não opina
73
+ if (lista.length > 1 && rand() < exploracao) {
74
+ const outros = lista.slice().sort((a, b) => amostras(conta(a)) - amostras(conta(b)));
75
+ const menos = amostras(conta(outros[0]));
76
+ const empatados = outros.filter((m) => amostras(conta(m)) === menos);
77
+ return { modelo: empatados[Math.floor(rand() * empatados.length) % empatados.length], motivo: 'explorando' };
78
+ }
79
+ let melhor = confiaveis[0];
80
+ for (const m of confiaveis) if (pontuacao(conta(m).ganhos, conta(m).perdas) > pontuacao(conta(melhor).ganhos, conta(melhor).perdas)) melhor = m;
81
+ return { modelo: melhor, motivo: 'lider' };
82
+ }
83
+
84
+ // ── o arquivo ────────────────────────────────────────────────────────
85
+ const arquivo = (home) => path.join(home || process.env.PRIMOCODE_PLACAR_DIR || path.join(os.homedir(), '.primocode'), 'placar.json'); // PRIMOCODE_PLACAR_DIR: o teste não suja o placar de verdade
86
+
87
+ function ler(home) {
88
+ try {
89
+ const j = JSON.parse(fs.readFileSync(arquivo(home), 'utf8'));
90
+ return j && typeof j === 'object' && j.tipos ? j : { v: 1, tipos: {} };
91
+ } catch { return { v: 1, tipos: {} }; }
92
+ }
93
+
94
+ function gravar(dados, home) {
95
+ try {
96
+ const f = arquivo(home);
97
+ fs.mkdirSync(path.dirname(f), { recursive: true });
98
+ fs.writeFileSync(f, JSON.stringify(dados, null, 2));
99
+ return true;
100
+ } catch { return false; }
101
+ }
102
+
103
+ let ultimo = null; // o último desfecho gravado: o /undo logo depois vira perda
104
+
105
+ function registrar(tipo, modelo, venceu, home) {
106
+ if (!TIPOS[tipo] || !modelo || typeof venceu !== 'boolean') return false;
107
+ const d = ler(home);
108
+ const m = idCurto(modelo);
109
+ const cel = ((d.tipos[tipo] = d.tipos[tipo] || {})[m] = d.tipos[tipo][m] || { ganhos: 0, perdas: 0 });
110
+ cel[venceu ? 'ganhos' : 'perdas']++;
111
+ ultimo = { tipo, modelo: m, venceu };
112
+ return gravar(d, home);
113
+ }
114
+
115
+ /** O /undo veio logo depois: o que contava como ganho vira perda. */
116
+ function desfez(home) {
117
+ if (!ultimo || !ultimo.venceu) return false;
118
+ const d = ler(home);
119
+ const cel = d.tipos[ultimo.tipo] && d.tipos[ultimo.tipo][ultimo.modelo];
120
+ if (!cel || cel.ganhos < 1) return false;
121
+ cel.ganhos--; cel.perdas++;
122
+ ultimo = { ...ultimo, venceu: false };
123
+ return gravar(d, home);
124
+ }
125
+
126
+ function zerar(home) { ultimo = null; return gravar({ v: 1, tipos: {} }, home); }
127
+
128
+ /** O desfecho de uma tarefa a partir dos sinais. true/false, ou null se não deu pra dizer. */
129
+ function veredito(s = {}, concluiu) {
130
+ if (!s.modelo) return null;
131
+ if (s.testes === false || s.diagSujo || concluiu === false) return false;
132
+ if (s.testes === true || s.diagLimpo || concluiu === true) return true;
133
+ return null;
134
+ }
135
+
136
+ /** Fecha uma tarefa: grava o desfecho. Vem do runAct (lib/act.js). */
137
+ function fechar(tipo, sinais, concluiu, home) {
138
+ const v = veredito(sinais, concluiu);
139
+ if (v === null) return null;
140
+ registrar(tipo, sinais.modelo, v, home);
141
+ return v;
142
+ }
143
+
144
+ // ── a tela do /placar ────────────────────────────────────────────────
145
+ function tabela(dados) {
146
+ const linhas = [];
147
+ for (const [tipo, rotulo] of Object.entries(TIPOS)) {
148
+ const cel = (dados.tipos && dados.tipos[tipo]) || {};
149
+ for (const [modelo, c] of Object.entries(cel)) {
150
+ const n = amostras(c);
151
+ if (!n) continue;
152
+ const taxa = Math.round((c.ganhos / n) * 100);
153
+ linhas.push({ tipo: rotulo, modelo, taxa, n, firme: n >= MIN_AMOSTRAS });
154
+ }
155
+ }
156
+ return linhas.sort((a, b) => a.tipo.localeCompare(b.tipo) || b.taxa - a.taxa || b.n - a.n);
157
+ }
158
+
159
+ function texto(dados) {
160
+ const t = tabela(dados);
161
+ if (!t.length) return 'O placar ainda está vazio. Conforme você usa o PrimoCode, ele anota qual modelo grátis acertou mais em cada tipo de tarefa (só neste computador).';
162
+ const w = Math.max(...t.map((x) => x.modelo.length), 6);
163
+ const out = ['Qual modelo grátis acerta mais, por tipo de tarefa (só neste computador):', ''];
164
+ let atual = '';
165
+ for (const x of t) {
166
+ if (x.tipo !== atual) { atual = x.tipo; out.push(` ${x.tipo}`); }
167
+ out.push(` ${x.modelo.padEnd(w)} acerta ${String(x.taxa).padStart(3)}% em ${x.n} ${x.n === 1 ? 'tarefa' : 'tarefas'}${x.firme ? '' : ' (ainda pouco dado)'}`);
168
+ }
169
+ out.push('', `Ele só passa a escolher sozinho depois de ${MIN_AMOSTRAS} tarefas por modelo, e de vez em quando testa outro. /placar zerar apaga tudo.`);
170
+ return out.join('\n');
171
+ }
172
+
173
+ module.exports = { TIPOS, MIN_AMOSTRAS, EXPLORACAO, classificar, pontuacao, escolher, ler, gravar, registrar, desfez, zerar, veredito, fechar, tabela, texto, arquivo };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * autoteste.js — o app se testa sozinho (ideia do Replit Agent). Depois de construir um app web:
3
+ * 1. sobe o app (lib/plugin/preview.js)
4
+ * 2. abre no navegador (lib/browser.js, o mesmo do agente)
5
+ * 3. clica nos fluxos principais (botões, links internos, submit de formulário), anotando erros do console,
6
+ * erros de página e prints
7
+ * 4. se falhou, devolve as falhas pro agente consertar — no máximo 2 rodadas
8
+ *
9
+ * `navegador` é injetável: { open(url), evaluate(js), clickSelector(sel), screenshot(path), wait(ms) } — o padrão é lib/browser.js.
10
+ * Sem Chrome/Playwright no computador o passo diz isso e não finge ter testado.
11
+ */
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const MAX_RODADAS = 2;
18
+
19
+ /** Injeta um coletor de erros na página (antes dos cliques). */
20
+ const COLETOR = `(function(){ if (window.__primoErros) return 1; window.__primoErros = [];
21
+ window.addEventListener('error', function(e){ window.__primoErros.push('erro: ' + (e.message || e)); });
22
+ window.addEventListener('unhandledrejection', function(e){ window.__primoErros.push('promessa sem catch: ' + ((e.reason && e.reason.message) || e.reason)); });
23
+ var ce = console.error; console.error = function(){ window.__primoErros.push('console.error: ' + Array.prototype.slice.call(arguments).join(' ')); ce.apply(console, arguments); };
24
+ return 1; })()`;
25
+
26
+ /** Os alvos clicáveis do fluxo principal (até `max`), como seletores estáveis. */
27
+ const ALVOS = `(function(max){
28
+ var out = [], vistos = {};
29
+ function sel(el){ if (el.id) return '#' + CSS.escape(el.id);
30
+ var i = Array.prototype.indexOf.call(el.parentNode.children, el) + 1;
31
+ return (el.parentNode === document.body ? 'body' : sel(el.parentNode)) + ' > ' + el.tagName.toLowerCase() + ':nth-child(' + i + ')'; }
32
+ document.querySelectorAll('button, [role=button], input[type=submit], input[type=button], a[href]').forEach(function(el){
33
+ if (out.length >= max) return;
34
+ var r = el.getBoundingClientRect(); if (!r.width || !r.height || el.disabled) return;
35
+ if (el.tagName === 'A') { var h = el.getAttribute('href') || ''; if (!/^(#|\\/|\\.)/.test(h) || h === '#') return; }
36
+ var s = sel(el); if (vistos[s]) return; vistos[s] = 1;
37
+ out.push({ seletor: s, nome: (el.innerText || el.value || el.getAttribute('aria-label') || el.tagName).trim().slice(0, 40) });
38
+ });
39
+ return out; })`;
40
+
41
+ /** Roda uma passada: devolve { ok, erros: [], prints: [], cliques: n }. */
42
+ async function passada({ url, navegador, dir, max = 6, nome = 'r1' }) {
43
+ const erros = [], prints = [];
44
+ const pasta = path.join(dir, '.primocode', 'autoteste');
45
+ fs.mkdirSync(pasta, { recursive: true });
46
+ await navegador.open(url);
47
+ await navegador.evaluate(COLETOR);
48
+ const alvos = (await navegador.evaluate(`${ALVOS}(${max})`)) || [];
49
+ const tira = async (rotulo) => { try { const r = await navegador.screenshot(path.join(pasta, `${nome}-${rotulo}.png`)); prints.push(r.path); } catch { /* sem print */ } };
50
+ await tira('abertura');
51
+ let cliques = 0;
52
+ for (const a of alvos) {
53
+ try { await navegador.clickSelector(a.seletor); cliques++; await (navegador.wait ? navegador.wait(400) : null); }
54
+ catch (e) { erros.push(`não consegui clicar em "${a.nome}": ${String(e.message || e).split('\n')[0]}`); }
55
+ await navegador.evaluate(COLETOR); // a página pode ter recarregado
56
+ const feitos = (await navegador.evaluate('(window.__primoErros || []).splice(0)').catch(() => [])) || [];
57
+ for (const f of feitos) erros.push(`ao clicar em "${a.nome}": ${f}`);
58
+ const atual = await navegador.evaluate('location.href').catch(() => url);
59
+ if (atual && !String(atual).startsWith(url.replace(/\/$/, ''))) { erros.push(`"${a.nome}" levou pra fora do app (${atual})`); await navegador.open(url); await navegador.evaluate(COLETOR); }
60
+ }
61
+ await tira('fim');
62
+ const finais = (await navegador.evaluate('(window.__primoErros || []).splice(0)').catch(() => [])) || [];
63
+ erros.push(...finais);
64
+ return { ok: !erros.length, erros: [...new Set(erros)], prints, cliques, alvos: alvos.length };
65
+ }
66
+
67
+ /**
68
+ * O laço completo. `consertar(falhas)` é chamado quando há erro (devolve uma Promise): é o agente corrigindo.
69
+ * Devolve { ok, rodadas, relatorio: [passada...], erro? }.
70
+ */
71
+ async function testar({ dir, navegador, subir, consertar, rodadasMax = MAX_RODADAS, log = () => {} }) {
72
+ subir = subir || ((d) => require('./preview.js').subir(d));
73
+ if (!navegador) { try { navegador = require('../browser.js'); } catch (e) { return { ok: false, rodadas: 0, relatorio: [], erro: 'sem navegador: ' + e.message }; } }
74
+ const app = await subir(dir);
75
+ if (!app.ok) return { ok: false, rodadas: 0, relatorio: [], erro: `não consegui subir o app: ${app.erro}` };
76
+ log(`app no ar em ${app.url}`);
77
+ const relatorio = [];
78
+ try {
79
+ for (let r = 0; r <= rodadasMax; r++) {
80
+ let p;
81
+ try { p = await passada({ url: app.url, navegador, dir, nome: `r${r + 1}` }); }
82
+ catch (e) { return { ok: false, rodadas: r, relatorio, erro: `o navegador não abriu (${String(e.message || e).split('\n')[0]}) — não testei` }; }
83
+ relatorio.push(p);
84
+ log(p.ok ? `rodada ${r + 1}: ${p.cliques} cliques, sem erros` : `rodada ${r + 1}: ${p.erros.length} problema(s)`);
85
+ if (p.ok) return { ok: true, rodadas: r, relatorio };
86
+ if (r === rodadasMax || !consertar) break;
87
+ await consertar(p.erros, p.prints); // até 2 consertos
88
+ }
89
+ return { ok: false, rodadas: Math.min(relatorio.length - 1, rodadasMax), relatorio, erro: 'ainda com erros depois dos consertos' };
90
+ } finally { try { app.parar(); } catch { /* */ } }
91
+ }
92
+
93
+ /** O pedido que volta pro agente com as falhas. */
94
+ function pedidoDeConserto(erros, prints = []) {
95
+ return `Testei o app abrindo e clicando nos fluxos principais e deu problema:\n${erros.map((e) => '- ' + e).join('\n')}\n`
96
+ + (prints.length ? `Prints: ${prints.join(', ')}\n` : '') + 'Conserte a causa de cada um e não mude o resto.';
97
+ }
98
+
99
+ module.exports = { testar, passada, pedidoDeConserto, COLETOR, MAX_RODADAS };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * edicao-visual.js — editar clicando (ideia do Lovable/v0). No preview local, um overlay injetado deixa a pessoa
3
+ * apontar um elemento da página; o PrimoCode captura:
4
+ * seletor CSS + outerHTML (cortado) + texto + palpite do arquivo-fonte
5
+ * pergunta "o que mudar?" (na própria página) e manda o pedido pro agente.
6
+ *
7
+ * injetar(html) → html com o overlay antes de </body>
8
+ * palpiteArquivo(dir, captura) → [{ arquivo, pontos }] (id, classes e texto procurados no código)
9
+ * montarPedido(captura, mudanca, p) → o texto que vai pro agente
10
+ * tratar(dir, aoEditar) → `extra(req,res)` pro servidor do preview: recebe o POST /__primo/editar
11
+ * proxiar(alvo, porta, opcoes) → servidor que repassa um dev server e injeta o overlay no HTML
12
+ *
13
+ * O palpite é só isso: um palpite (grep), dito como tal no pedido. O agente confere no arquivo.
14
+ */
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const http = require('http');
19
+ const path = require('path');
20
+
21
+ const ROTA = '/__primo/editar';
22
+ const MARCA = 'data-primo-overlay';
23
+
24
+ const OVERLAY = `(function(){
25
+ if (window.__primoOverlay) return; window.__primoOverlay = 1;
26
+ var ligado = false, cx = document.createElement('div');
27
+ cx.setAttribute('${MARCA}', '1');
28
+ cx.style.cssText = 'position:fixed;pointer-events:none;border:2px solid #7c5cff;background:rgba(124,92,255,.12);z-index:2147483646;display:none;border-radius:3px';
29
+ var bt = document.createElement('button'); bt.setAttribute('${MARCA}', '1'); bt.textContent = '✎ Editar';
30
+ bt.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;padding:10px 14px;border:0;border-radius:999px;background:#7c5cff;color:#fff;font:600 13px system-ui;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.3)';
31
+ document.body.appendChild(cx); document.body.appendChild(bt);
32
+ function ehNosso(el){ return el && el.closest && el.closest('[${MARCA}]'); }
33
+ function seletor(el){
34
+ if (el.id) return '#' + CSS.escape(el.id);
35
+ if (el === document.body) return 'body';
36
+ var i = Array.prototype.indexOf.call(el.parentNode.children, el) + 1;
37
+ return seletor(el.parentNode) + ' > ' + el.tagName.toLowerCase() + ':nth-child(' + i + ')';
38
+ }
39
+ bt.addEventListener('click', function(e){ e.stopPropagation(); ligado = !ligado; bt.textContent = ligado ? '✕ Sair da edição' : '✎ Editar'; cx.style.display = 'none'; document.body.style.cursor = ligado ? 'crosshair' : ''; });
40
+ document.addEventListener('mouseover', function(e){ if (!ligado || ehNosso(e.target)) return; var r = e.target.getBoundingClientRect();
41
+ cx.style.display = 'block'; cx.style.left = r.left + 'px'; cx.style.top = r.top + 'px'; cx.style.width = r.width + 'px'; cx.style.height = r.height + 'px'; }, true);
42
+ document.addEventListener('click', function(e){
43
+ if (!ligado || ehNosso(e.target)) return;
44
+ e.preventDefault(); e.stopPropagation();
45
+ var el = e.target, cap = { url: location.pathname, seletor: seletor(el), html: el.outerHTML.slice(0, 2000), texto: (el.innerText || '').trim().slice(0, 200), tag: el.tagName.toLowerCase(), id: el.id || '', classes: (el.getAttribute('class') || '') };
46
+ var mud = window.prompt('O que mudar neste elemento?\\n' + cap.seletor);
47
+ if (!mud) return;
48
+ cap.mudanca = mud;
49
+ fetch('${ROTA}', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(cap) })
50
+ .then(function(r){ return r.json(); }).then(function(j){ bt.textContent = j && j.ok ? '✓ Enviado ao PrimoCode' : '! Falhou'; setTimeout(function(){ bt.textContent = ligado ? '✕ Sair da edição' : '✎ Editar'; }, 2500); })
51
+ .catch(function(){ bt.textContent = '! Falhou'; });
52
+ }, true);
53
+ })();`;
54
+
55
+ function injetar(html) {
56
+ if (String(html).includes('__primoOverlay')) return html;
57
+ const tag = `<script ${MARCA}="1">${OVERLAY}</script>`;
58
+ return /<\/body>/i.test(html) ? html.replace(/<\/body>/i, () => tag + '</body>') : html + tag;
59
+ }
60
+
61
+ const PULA = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.primocode', 'pesquisas']);
62
+ const EXTS = new Set(['.html', '.htm', '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', '.css', '.scss', '.astro', '.md']);
63
+
64
+ function* arquivos(dir, prof = 0) {
65
+ if (prof > 6) return;
66
+ let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
67
+ for (const e of ents) {
68
+ if (PULA.has(e.name) || e.name.startsWith('.')) continue;
69
+ const p = path.join(dir, e.name);
70
+ if (e.isDirectory()) yield* arquivos(p, prof + 1);
71
+ else if (EXTS.has(path.extname(e.name).toLowerCase())) yield p;
72
+ }
73
+ }
74
+
75
+ /** Procura no código o id, as classes e o texto do elemento. Mais pontos = mais provável. */
76
+ function palpiteArquivo(dir, cap, max = 3) {
77
+ const classes = String(cap.classes || '').split(/\s+/).filter((c) => c.length > 2);
78
+ const texto = String(cap.texto || '').split('\n')[0].trim().slice(0, 60);
79
+ const achados = [];
80
+ for (const p of arquivos(dir)) {
81
+ let s; try { const st = fs.statSync(p); if (st.size > 400 * 1024) continue; s = fs.readFileSync(p, 'utf8'); } catch { continue; }
82
+ let pontos = 0;
83
+ if (cap.id && (s.includes(`id="${cap.id}"`) || s.includes(`id='${cap.id}'`) || s.includes(`#${cap.id}`))) pontos += 4;
84
+ for (const c of classes) if (s.includes(c)) pontos += 1;
85
+ if (texto.length > 3 && s.includes(texto)) pontos += 3;
86
+ if (pontos) achados.push({ arquivo: path.relative(dir, p), pontos });
87
+ }
88
+ return achados.sort((a, b) => b.pontos - a.pontos).slice(0, max);
89
+ }
90
+
91
+ function montarPedido(cap, mudanca, palpites = []) {
92
+ return [`Edição visual: na página ${cap.url || '/'}, o usuário clicou num elemento e quer: ${mudanca}`, '',
93
+ `Seletor: ${cap.seletor}`, `Texto: ${cap.texto || '(sem texto)'}`, 'HTML do elemento:', '```html', cap.html || '', '```',
94
+ palpites.length ? `Palpite de arquivo (grep, confira): ${palpites.map((p) => p.arquivo).join(', ')}` : 'Não achei o arquivo-fonte por grep: procure pelo texto/classes.',
95
+ 'Faça só essa mudança e não mexa no resto da página.'].join('\n');
96
+ }
97
+
98
+ /** Handler pro servidor do preview: true se tratou a requisição. `aoEditar(pedido, cap)` é quem manda pro agente. */
99
+ function tratar(dir, aoEditar) {
100
+ return (req, res) => {
101
+ if (req.method !== 'POST' || !(req.url || '').startsWith(ROTA)) return false;
102
+ let corpo = ''; req.on('data', (b) => { corpo += b; if (corpo.length > 200000) req.destroy(); });
103
+ req.on('end', async () => {
104
+ let cap; try { cap = JSON.parse(corpo); } catch { res.writeHead(400); return res.end('{"ok":false}'); }
105
+ if (!cap || !cap.seletor || !cap.mudanca) { res.writeHead(400); return res.end('{"ok":false}'); }
106
+ const pedido = montarPedido(cap, String(cap.mudanca).slice(0, 500), palpiteArquivo(dir, cap));
107
+ try { await aoEditar(pedido, cap); res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"ok":true}'); }
108
+ catch { res.writeHead(500); res.end('{"ok":false}'); }
109
+ });
110
+ return true;
111
+ };
112
+ }
113
+
114
+ /** Repassa um dev server (alvo = 'http://localhost:5173') injetando o overlay no HTML. */
115
+ function proxiar(alvo, porta, { dir, aoEditar }) {
116
+ const u = new URL(alvo);
117
+ const meu = tratar(dir, aoEditar);
118
+ const srv = http.createServer((req, res) => {
119
+ if (meu(req, res)) return;
120
+ const headers = { ...req.headers, host: u.host, 'accept-encoding': 'identity' };
121
+ const pr = http.request({ host: u.hostname, port: u.port, path: req.url, method: req.method, headers }, (r) => {
122
+ const html = /text\/html/.test(r.headers['content-type'] || '');
123
+ if (!html) { res.writeHead(r.statusCode, r.headers); return r.pipe(res); }
124
+ let b = ''; r.setEncoding('utf8'); r.on('data', (x) => { b += x; });
125
+ r.on('end', () => { const h = { ...r.headers }; const corpo = injetar(b); delete h['content-length']; res.writeHead(r.statusCode, h); res.end(corpo); });
126
+ });
127
+ pr.on('error', () => { res.writeHead(502); res.end('o app não respondeu'); });
128
+ req.pipe(pr);
129
+ });
130
+ return new Promise((ok) => srv.listen(porta, '127.0.0.1', () => ok(srv)));
131
+ }
132
+
133
+ module.exports = { OVERLAY, ROTA, injetar, palpiteArquivo, montarPedido, tratar, proxiar };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * pesquisar.js — `/pesquisar <tema>`: pesquisa profunda com fontes.
3
+ * 1. o modelo quebra o tema em 3-5 sub-perguntas (JSON)
4
+ * 2. cada uma passa por `buscar` (a ferramenta buscar_web, lib/tool/web.js;
5
+ * aqui só a interface `buscar(consulta) -> [{titulo, url, trecho}]`. Sem ela, o relatório diz que não buscou)
6
+ * 3. as melhores URLs são lidas (lib/web-pagina.js)
7
+ * 4. o modelo escreve o relatório em markdown citando [1], [2]… e só as fontes lidas entram na lista
8
+ * 5. grava em pesquisas/<tema>.md
9
+ * Nada é inventado: sem fonte lida, a seção diz "sem fonte" em vez de afirmar.
10
+ */
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { acharJson, perguntarPadrao, slug } = require('../cli/util.js');
16
+
17
+ const MAX_PERGUNTAS = 5, URLS_POR_PERGUNTA = 2;
18
+
19
+ async function buscarPadrao() {
20
+ const { buscarWeb } = require('../tool/web.js'); // a ferramenta buscar_web de verdade
21
+ return async (consulta) => {
22
+ const r = await buscarWeb({ consulta, limite: URLS_POR_PERGUNTA + 2 });
23
+ if (!r.ok) throw new Error(r.error || 'buscar_web não achou nada');
24
+ return r.resultados;
25
+ };
26
+ }
27
+
28
+ /** O plano: sub-perguntas. Se o modelo não devolver JSON, usa o próprio tema. */
29
+ async function planejar(tema, perguntar) {
30
+ const bruto = await perguntar(null, 'Quebre o tema em 3 a 5 sub-perguntas de pesquisa, curtas e distintas. Devolva SÓ um JSON: {"perguntas":["..."]}.', `Tema: ${tema}`);
31
+ const j = acharJson(bruto);
32
+ const l = (j && (Array.isArray(j) ? j : j.perguntas)) || [];
33
+ const ps = l.filter((x) => typeof x === 'string' && x.trim()).map((x) => x.trim()).slice(0, MAX_PERGUNTAS);
34
+ return ps.length ? ps : [tema];
35
+ }
36
+
37
+ async function pesquisar({ tema, dir = process.cwd(), perguntar, buscar, ler, log = () => {}, agora = () => new Date() }) {
38
+ tema = String(tema || '').trim();
39
+ if (!tema) return { ok: false, erro: 'Faltou o tema. Exemplo: /pesquisar como funciona cache no edge' };
40
+ perguntar = perguntar || perguntarPadrao;
41
+ ler = ler || (async (url) => { const r = await require('../web-pagina.js').baixar(url); return r.ok ? { titulo: r.titulo, texto: r.texto } : null; });
42
+ const avisos = [];
43
+ try { buscar = buscar || await buscarPadrao(); } catch (e) { buscar = null; avisos.push(e.message); }
44
+
45
+ const perguntas = await planejar(tema, perguntar);
46
+ log(`${perguntas.length} sub-pergunta(s)`);
47
+ const fontes = [], porPergunta = [];
48
+ for (const p of perguntas) {
49
+ const achadas = [];
50
+ if (buscar) {
51
+ let rs = [];
52
+ try { rs = (await buscar(p)) || []; } catch (e) { avisos.push(`busca "${p}": ${e.message}`); }
53
+ for (const r of rs.slice(0, URLS_POR_PERGUNTA)) {
54
+ if (!r || !r.url) continue;
55
+ let f = fontes.find((x) => x.url === r.url);
56
+ if (!f) {
57
+ let lido = null;
58
+ try { lido = await ler(r.url); } catch { /* segue sem */ }
59
+ if (!lido || !lido.texto) { avisos.push(`não consegui ler ${r.url}`); continue; }
60
+ f = { n: fontes.length + 1, url: r.url, titulo: r.titulo || lido.titulo || r.url, texto: String(lido.texto).slice(0, 6000) };
61
+ fontes.push(f);
62
+ }
63
+ achadas.push(f);
64
+ }
65
+ }
66
+ porPergunta.push({ pergunta: p, fontes: achadas });
67
+ log(`"${p}" → ${achadas.length} fonte(s) lida(s)`);
68
+ }
69
+
70
+ let corpo;
71
+ if (!fontes.length) {
72
+ corpo = perguntas.map((p) => `## ${p}\n\nSem fonte: não consegui buscar nem ler nada pra esta pergunta.\n`).join('\n');
73
+ } else {
74
+ const material = porPergunta.map((x) => `### ${x.pergunta}\n` + (x.fontes.length ? x.fontes.map((f) => `[${f.n}] ${f.titulo} (${f.url})\n${f.texto}`).join('\n\n') : '(sem fonte)')).join('\n\n');
75
+ corpo = await perguntar(null, 'Escreva um relatório em markdown (PT-BR), uma seção "## pergunta" por sub-pergunta. Use SÓ o material dado; cite cada afirmação com [n]. Pergunta sem fonte: escreva "Sem fonte." Não invente.', `Tema: ${tema}\n\n${material}`);
76
+ }
77
+ const md = [`# ${tema}`, '', `_Pesquisa de ${agora().toISOString().slice(0, 10)} · ${fontes.length} fonte(s) lida(s)_`, '',
78
+ ...(avisos.length ? ['> Avisos: ' + [...new Set(avisos)].join(' · '), ''] : []), String(corpo).trim(), '',
79
+ '## Fontes', '', ...(fontes.length ? fontes.map((f) => `${f.n}. [${f.titulo}](${f.url})`) : ['_nenhuma_']), ''].join('\n');
80
+ const arquivo = path.join(dir, 'pesquisas', slug(tema) + '.md');
81
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
82
+ fs.writeFileSync(arquivo, md);
83
+ return { ok: true, arquivo, fontes: fontes.length, avisos, perguntas };
84
+ }
85
+
86
+ async function main(args) {
87
+ const r = await pesquisar({ tema: args.filter((a) => !a.startsWith('-')).join(' '), log: (l) => console.log(' ' + l) });
88
+ console.log(r.ok ? ` Relatório: ${r.arquivo}${r.avisos.length ? '\n Avisos: ' + r.avisos.join(' · ') : ''}` : ' ' + r.erro);
89
+ return r.ok ? 0 : 1;
90
+ }
91
+
92
+ module.exports = { pesquisar, planejar, main };