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.
- package/LICENSES/opencode-MIT.txt +24 -0
- package/LICENSES/pptxgenjs-MIT.txt +31 -0
- package/README.md +16 -3
- package/agentes/acessibilidade.md +10 -0
- package/agentes/ambiente.md +10 -0
- package/agentes/api.md +10 -0
- package/agentes/backend.md +10 -0
- package/agentes/copy.md +10 -0
- package/agentes/css-layout.md +10 -0
- package/agentes/dados.md +10 -0
- package/agentes/depuracao.md +10 -0
- package/agentes/design.md +10 -0
- package/agentes/docs.md +10 -0
- package/agentes/frontend.md +10 -0
- package/agentes/geral/capacidades/arquivos.json +132 -0
- package/agentes/geral/capacidades/codigo.json +132 -0
- package/agentes/geral/capacidades/dados.json +126 -0
- package/agentes/geral/capacidades/depuracao.json +126 -0
- package/agentes/geral/capacidades/escrita.json +126 -0
- package/agentes/geral/capacidades/git.json +126 -0
- package/agentes/geral/capacidades/pesquisa.json +126 -0
- package/agentes/geral/capacidades/seguranca.json +132 -0
- package/agentes/geral/capacidades/shell.json +132 -0
- package/agentes/geral/capacidades/web.json +126 -0
- package/agentes/git-pr.md +10 -0
- package/agentes/indice.md +86 -0
- package/agentes/nextjs.md +10 -0
- package/agentes/node.md +10 -0
- package/agentes/performance.md +10 -0
- package/agentes/pesquisa.md +10 -0
- package/agentes/python.md +10 -0
- package/agentes/react.md +10 -0
- package/agentes/refatoracao.md +10 -0
- package/agentes/revisor.md +10 -0
- package/agentes/seguranca.md +10 -0
- package/agentes/sql.md +10 -0
- package/agentes/testes.md +10 -0
- package/agentes/typescript.md +10 -0
- package/agentes/ux.md +10 -0
- package/bin/primocode.js +65 -8
- package/editor/helpers/camera_viva.py +28 -2
- package/lib/act.js +79 -10
- package/lib/agentes/index.js +146 -0
- package/lib/agentes/nucleo.js +18 -0
- package/lib/catalogo.js +12 -2
- package/lib/cli/review.js +64 -0
- package/lib/cli/subcomandos.js +34 -0
- package/lib/cli/tentativas.js +107 -0
- package/lib/cli/util.js +51 -0
- package/lib/comandos-extra.js +4 -2
- package/lib/editor-pro-maquina.js +165 -0
- package/lib/editor-pro.js +10 -7
- package/lib/entregaveis/cli.js +26 -0
- package/lib/entregaveis/especialistas.js +50 -0
- package/lib/entregaveis/nucleo.js +111 -0
- package/lib/entregaveis/slides.js +221 -0
- package/lib/estudio/materiais.js +172 -0
- package/lib/fundo.js +34 -1
- package/lib/kits.js +41 -2
- package/lib/nomes.js +6 -0
- package/lib/placar.js +173 -0
- package/lib/plugin/autoteste.js +99 -0
- package/lib/plugin/edicao-visual.js +133 -0
- package/lib/plugin/pesquisar.js +92 -0
- package/lib/plugin/preview-cli.js +38 -0
- package/lib/plugin/preview.js +89 -0
- package/lib/plugin/tunel.js +27 -0
- package/lib/review.js +134 -0
- package/lib/session/compactacao.js +124 -0
- package/lib/statusline.js +52 -0
- package/lib/tool/diagnostico.js +100 -0
- package/lib/tool/perguntar.js +60 -0
- package/lib/tool/reparar-entrada.js +66 -0
- package/lib/tool/web.js +102 -0
- package/lib/tools.js +20 -1
- package/lib/ui.js +1 -1
- package/materiais/saas-linear/base.ts +84 -0
- package/materiais/saas-linear/camera/Camera.tsx +38 -0
- package/materiais/saas-linear/fontes/OFL-Inter.txt +93 -0
- package/materiais/saas-linear/fontes/OFL-Newsreader.txt +93 -0
- package/materiais/saas-linear/fontes/inter-400.woff2 +0 -0
- package/materiais/saas-linear/fontes/inter-600.woff2 +0 -0
- package/materiais/saas-linear/fontes/newsreader-400.woff2 +0 -0
- package/materiais/saas-linear/formas/Agenda.tsx +46 -0
- package/materiais/saas-linear/formas/CaixaPrompt.tsx +41 -0
- package/materiais/saas-linear/formas/CartoesIntegracao.tsx +57 -0
- package/materiais/saas-linear/formas/Cursor.tsx +40 -0
- package/materiais/saas-linear/formas/Menu.tsx +46 -0
- package/materiais/saas-linear/formas/Monitor.tsx +28 -0
- package/materiais/saas-linear/paleta.json +18 -0
- package/materiais/saas-linear/pecas.json +590 -0
- package/materiais/saas-linear/regras.md +40 -0
- package/materiais/saas-linear/sons/Som.tsx +39 -0
- package/materiais/saas-linear/sons/assinatura.mp3 +0 -0
- package/materiais/saas-linear/sons/clique.mp3 +0 -0
- package/materiais/saas-linear/sons/impacto.mp3 +0 -0
- package/materiais/saas-linear/sons/pop-cartao.mp3 +0 -0
- package/materiais/saas-linear/sons/pop-palavra.mp3 +0 -0
- package/materiais/saas-linear/sons/subida.mp3 +0 -0
- package/materiais/saas-linear/sons/swoosh.mp3 +0 -0
- package/materiais/saas-linear/sons/trilha.mp3 +0 -0
- package/materiais/saas-linear/sons/whoosh-entra.mp3 +0 -0
- package/materiais/saas-linear/sons/whoosh-sai.mp3 +0 -0
- package/materiais/saas-linear/texturas/Fundo.tsx +18 -0
- package/materiais/saas-linear/texturas/Particulas.tsx +22 -0
- package/materiais/saas-linear/tipografia/FraseSerifa.tsx +25 -0
- package/materiais/saas-linear/tipografia/Marca.tsx +20 -0
- package/materiais/saas-linear/tipografia/MarcaFinal.tsx +22 -0
- package/materiais/saas-linear/tipografia/Saudacao.tsx +27 -0
- package/materiais/saas-linear/transicoes/Cena.tsx +38 -0
- package/materiais/saas-linear/transicoes/CorteImpacto.tsx +16 -0
- package/package.json +3 -1
- package/skills/deck-conferido/SKILL.md +29 -0
- package/vendor/pptxgenjs/carregar.js +12 -0
- package/vendor/pptxgenjs/jszip.min.js +13 -0
- 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 };
|