primocode 10.15.2 → 10.17.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/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +74 -9
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +38 -8
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +7 -1
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto/tela.html +1349 -0
- package/lib/estudio/gosto.js +305 -0
- package/lib/estudio/index.js +12 -1
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +70 -12
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +64 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/headless.js +15 -3
- package/lib/local/cerebro.json +285 -7
- package/lib/local/motor.js +5 -2
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +33 -2
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +23 -4
- package/lib/ui.js +2 -2
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/__main__.py +55 -40
- package/voz/primo_voz/app.py +500 -4
- package/voz/primo_voz/chat.html +1773 -0
- package/voz/primo_voz/conversa.py +0 -26
- package/voz/primo_voz/hud.html +8 -2
- package/voz/primo_voz/hud.py +129 -5
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memoria-usuario.js — o PrimoCode lembra de quem a pessoa é.
|
|
3
|
+
*
|
|
4
|
+
* A memória que vale entre sessões, do jeito do Claude Code: um fato por
|
|
5
|
+
* arquivo .md em ~/.primocode/memoria/, e um índice MEMORIA.md com uma linha
|
|
6
|
+
* por fato. O índice entra no contexto de TODA sessão; o corpo de cada fato
|
|
7
|
+
* o modelo lê quando precisa (read_file no caminho).
|
|
8
|
+
*
|
|
9
|
+
* ~/.primocode/memoria/MEMORIA.md - [Título](arquivo.md) — gancho
|
|
10
|
+
* ~/.primocode/memoria/<nome-em-kebab>.md frontmatter (nome, descricao, tipo) + corpo
|
|
11
|
+
*
|
|
12
|
+
* Difere do memory-geral.md (uma lista corrida de frases) e do AGENTS.md da
|
|
13
|
+
* pasta (o resumo da conversa): aqui cada fato tem nome, tipo e pode ser
|
|
14
|
+
* atualizado ou apagado sem duplicar.
|
|
15
|
+
*
|
|
16
|
+
* HOME é injetável, como em lib/estudio/gosto.js: o teste usa uma casa temporária.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const TIPOS = ['usuario', 'feedback', 'projeto', 'referencia'];
|
|
26
|
+
const LINHAS_DO_INDICE = 200;
|
|
27
|
+
const LIMITE_TEXTO = 8000;
|
|
28
|
+
const INDICE = 'MEMORIA.md';
|
|
29
|
+
|
|
30
|
+
// Cara de segredo: o fato fica em disco e volta em todo prompt, então chave não entra.
|
|
31
|
+
const SEGREDOS = [
|
|
32
|
+
/\bsk-[A-Za-z0-9_-]{8,}/,
|
|
33
|
+
/\bAIza[0-9A-Za-z_-]{10,}/,
|
|
34
|
+
/\bgh[pousr]_[A-Za-z0-9]{10,}/,
|
|
35
|
+
/-----BEGIN [A-Z ]*(?:PRIVATE KEY|CERTIFICATE)?-----/,
|
|
36
|
+
/\bxox[abprs]-[A-Za-z0-9-]{8,}/,
|
|
37
|
+
/\bAKIA[0-9A-Z]{12,}/,
|
|
38
|
+
/\b(?:senha|password|passwd|token|api[_-]?key|secret)\s*[:=]\s*\S+/i,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function pasta(home = os.homedir()) { return path.join(home, '.primocode', 'memoria'); }
|
|
42
|
+
function arquivoIndice(home) { return path.join(pasta(home), INDICE); }
|
|
43
|
+
|
|
44
|
+
/** "Prefere Respostas Curtas!" → "prefere-respostas-curtas". '' se não sobrar nada. */
|
|
45
|
+
function kebab(s) {
|
|
46
|
+
return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60).replace(/-+$/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function umaLinha(s) { return String(s || '').replace(/\s+/g, ' ').trim(); }
|
|
51
|
+
|
|
52
|
+
/** O texto parece ter segredo? Devolve true/false. Puro. */
|
|
53
|
+
function temSegredo(...textos) {
|
|
54
|
+
return textos.some((t) => SEGREDOS.some((re) => re.test(String(t || ''))));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** "usuario-prefere-curto" → "Usuario prefere curto": o título que aparece no índice. */
|
|
58
|
+
function titulo(nome) {
|
|
59
|
+
const t = String(nome || '').replace(/-/g, ' ');
|
|
60
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function montar({ nome, descricao, tipo, texto }) {
|
|
64
|
+
return `---\nnome: ${nome}\ndescricao: ${descricao}\ntipo: ${tipo}\n---\n\n${String(texto).trim()}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Lê o frontmatter simples (chave: valor) e o corpo. null se não tem. */
|
|
68
|
+
function analisar(conteudo) {
|
|
69
|
+
const m = String(conteudo || '').match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
70
|
+
if (!m) return null;
|
|
71
|
+
const meta = {};
|
|
72
|
+
for (const l of m[1].split('\n')) {
|
|
73
|
+
const i = l.indexOf(':');
|
|
74
|
+
if (i > 0) meta[l.slice(0, i).trim()] = l.slice(i + 1).trim();
|
|
75
|
+
}
|
|
76
|
+
return { ...meta, corpo: m[2].trim() };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Todas as memórias, em ordem alfabética do arquivo. */
|
|
80
|
+
function listar(home) {
|
|
81
|
+
let nomes = [];
|
|
82
|
+
try { nomes = fs.readdirSync(pasta(home)); } catch { return []; }
|
|
83
|
+
const saida = [];
|
|
84
|
+
for (const f of nomes.sort()) {
|
|
85
|
+
if (!f.endsWith('.md') || f === INDICE) continue;
|
|
86
|
+
let a = null;
|
|
87
|
+
try { a = analisar(fs.readFileSync(path.join(pasta(home), f), 'utf-8')); } catch { /* ilegível: pula */ }
|
|
88
|
+
if (!a) continue;
|
|
89
|
+
saida.push({ nome: a.nome || f.slice(0, -3), descricao: a.descricao || '', tipo: a.tipo || '', arquivo: f, corpo: a.corpo });
|
|
90
|
+
}
|
|
91
|
+
return saida;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Refaz o MEMORIA.md a partir dos arquivos: o índice nunca desanda do que existe. */
|
|
95
|
+
function reindexar(home) {
|
|
96
|
+
const linhas = listar(home).map((m) => `- [${titulo(m.nome)}](${m.arquivo}) — ${m.descricao}`);
|
|
97
|
+
fs.mkdirSync(pasta(home), { recursive: true });
|
|
98
|
+
fs.writeFileSync(arquivoIndice(home), linhas.length ? linhas.join('\n') + '\n' : '', 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Salva ou atualiza (mesmo nome = mesmo arquivo, sem duplicar).
|
|
103
|
+
* Devolve { ok, nome, arquivo, criado } ou { ok:false, error }.
|
|
104
|
+
*/
|
|
105
|
+
function salvar({ nome, descricao, tipo, texto } = {}, home) {
|
|
106
|
+
const slug = kebab(nome);
|
|
107
|
+
if (!slug) return { ok: false, error: 'Dê um nome curto pra memória (ex.: "prefere-respostas-curtas").' };
|
|
108
|
+
const desc = umaLinha(descricao);
|
|
109
|
+
if (!desc) return { ok: false, error: 'Falta a descrição: uma linha dizendo do que se trata.' };
|
|
110
|
+
const t = String(tipo || '').toLowerCase();
|
|
111
|
+
if (!TIPOS.includes(t)) return { ok: false, error: `Tipo inválido. Use um destes: ${TIPOS.join(', ')}.` };
|
|
112
|
+
const corpo = String(texto || '').trim();
|
|
113
|
+
if (!corpo) return { ok: false, error: 'Falta o texto da memória.' };
|
|
114
|
+
if (corpo.length > LIMITE_TEXTO) return { ok: false, error: `Texto grande demais (máx. ${LIMITE_TEXTO} letras). Resuma.` };
|
|
115
|
+
if (temSegredo(nome, desc, corpo)) {
|
|
116
|
+
return { ok: false, error: 'Isso tem cara de segredo (chave, token ou senha). Não guardo isso na memória. Tire o segredo e tente de novo.' };
|
|
117
|
+
}
|
|
118
|
+
const arquivo = `${slug}.md`;
|
|
119
|
+
const caminho = path.join(pasta(home), arquivo);
|
|
120
|
+
const criado = !fs.existsSync(caminho);
|
|
121
|
+
try {
|
|
122
|
+
fs.mkdirSync(pasta(home), { recursive: true });
|
|
123
|
+
fs.writeFileSync(caminho, montar({ nome: slug, descricao: desc, tipo: t, texto: corpo }), 'utf-8');
|
|
124
|
+
reindexar(home);
|
|
125
|
+
} catch (e) { return { ok: false, error: `Não consegui gravar: ${e.message}` }; }
|
|
126
|
+
return { ok: true, nome: slug, arquivo, criado };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Apaga pelo nome. Devolve { ok } ou { ok:false, error }. */
|
|
130
|
+
function apagar(nome, home) {
|
|
131
|
+
const slug = kebab(nome);
|
|
132
|
+
const caminho = path.join(pasta(home), `${slug}.md`);
|
|
133
|
+
if (!slug || !fs.existsSync(caminho)) return { ok: false, error: `Não achei a memória "${nome}".` };
|
|
134
|
+
try { fs.unlinkSync(caminho); reindexar(home); } catch (e) { return { ok: false, error: `Não consegui apagar: ${e.message}` }; }
|
|
135
|
+
return { ok: true, nome: slug };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** O índice, cortado em `max` linhas (a cabeça fica). '' se não há memória. */
|
|
139
|
+
function carregarIndice(home, max = LINHAS_DO_INDICE) {
|
|
140
|
+
let t = '';
|
|
141
|
+
try { t = fs.readFileSync(arquivoIndice(home), 'utf-8'); } catch { return ''; }
|
|
142
|
+
const linhas = t.split('\n').filter((l) => l.trim());
|
|
143
|
+
if (linhas.length <= max) return linhas.join('\n');
|
|
144
|
+
return linhas.slice(0, max).join('\n') + `\n(… cortado: mais ${linhas.length - max} memórias, use /memoria)`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** As regras de quando salvar. Entram no contexto junto com o índice. */
|
|
148
|
+
const INSTRUCAO = '[memória]\n'
|
|
149
|
+
+ 'Você tem memória que dura entre sessões, em ~/.primocode/memoria/ (ferramenta `lembrar`: ação salvar ou apagar). '
|
|
150
|
+
+ 'O índice está abaixo; para ler uma memória, read_file no arquivo dela.\n'
|
|
151
|
+
+ '- SALVE quando a pessoa corrige um jeito de trabalhar, diz uma preferência, conta quem é, ou dá um fato de projeto que não está no código. '
|
|
152
|
+
+ 'tipo: usuario (quem é, como trabalha), feedback (correção ou preferência), projeto (fato do projeto), referencia (onde achar algo).\n'
|
|
153
|
+
+ '- NÃO salve o que o código ou o git já mostram, nem o que só vale para esta conversa. Nunca salve chave, token ou senha.\n'
|
|
154
|
+
+ '- ANTES de salvar, confira o índice: se já existe uma que cobre, salve com o MESMO nome para atualizar, em vez de duplicar.';
|
|
155
|
+
|
|
156
|
+
/** O bloco que entra no contexto da sessão: instrução + índice. */
|
|
157
|
+
/* No servidor (plano grátis) o contexto fixo é curto: o índice entra só com as primeiras `linhas`, e o resto
|
|
158
|
+
vira o aviso de corte. Com provedor próprio (/api) entra até LINHAS_DO_INDICE. */
|
|
159
|
+
function blocoDoContexto(home, { linhas = LINHAS_DO_INDICE } = {}) {
|
|
160
|
+
const indice = carregarIndice(home, linhas);
|
|
161
|
+
return INSTRUCAO + (indice ? `\n\n[MEMORIA.md — o que você já sabe sobre esta pessoa]\n${indice}` : '\n\n(Ainda não há memórias salvas.)');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { TIPOS, INDICE, LINHAS_DO_INDICE, pasta, arquivoIndice, kebab, temSegredo, listar, salvar, apagar, reindexar, carregarIndice, blocoDoContexto, INSTRUCAO };
|
package/lib/nuvem.js
CHANGED
|
@@ -300,6 +300,27 @@ async function lerConversa(id, { limite = 200 } = {}) {
|
|
|
300
300
|
} catch (e) { return { ok: false, error: e.message }; }
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Apaga uma conversa. O Firestore não apaga a subcoleção junto com o
|
|
305
|
+
* documento, então as mensagens vão antes, em lotes, e a conversa por último.
|
|
306
|
+
*/
|
|
307
|
+
async function apagarConversa(id) {
|
|
308
|
+
const s = await abrir();
|
|
309
|
+
if (!s) return { ok: false, error: 'não conectado' };
|
|
310
|
+
const base = `${BASE(s.projeto)}/users/${s.uid}/${COLECAO}/${id}`;
|
|
311
|
+
const auth = { Authorization: `Bearer ${s.idToken}` };
|
|
312
|
+
try {
|
|
313
|
+
for (let volta = 0; volta < 50; volta++) {
|
|
314
|
+
const r = await httpsJson(`${base}/messages?pageSize=300`, 'GET', null, auth);
|
|
315
|
+
const docs = r.documents || [];
|
|
316
|
+
if (!docs.length) break;
|
|
317
|
+
for (const d of docs) await httpsJson(`${FIRESTORE}/v1/${d.name}`, 'DELETE', null, auth);
|
|
318
|
+
}
|
|
319
|
+
await httpsJson(base, 'DELETE', null, auth);
|
|
320
|
+
return { ok: true };
|
|
321
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
322
|
+
}
|
|
323
|
+
|
|
303
324
|
// ── o gravador da sessão ─────────────────────────────────────────────────
|
|
304
325
|
|
|
305
326
|
/** Título a partir da primeira frase: uma linha, curta, sem cortar no meio de
|
|
@@ -331,7 +352,7 @@ function tituloDe(texto) {
|
|
|
331
352
|
* terminal e simplesmente não sobe. Derrubar o turno de trabalho de alguém
|
|
332
353
|
* porque o histórico não salvou seria trocar um incômodo por um prejuízo.
|
|
333
354
|
*/
|
|
334
|
-
function gravador() {
|
|
355
|
+
function gravador({ batizar } = {}) {
|
|
335
356
|
let id = null;
|
|
336
357
|
let titulo = null;
|
|
337
358
|
let contagem = 0;
|
|
@@ -360,6 +381,7 @@ function gravador() {
|
|
|
360
381
|
const s = await abrir();
|
|
361
382
|
if (!s) return; // sem conta: fica só no local
|
|
362
383
|
if (!titulo) titulo = tituloDe(pergunta);
|
|
384
|
+
const nova = !id; // só a primeira troca de uma conversa nova ganha título da IA
|
|
363
385
|
if (!id) {
|
|
364
386
|
const r = await salvarConversa({ titulo });
|
|
365
387
|
if (!r.ok) { ultimoErro = r.error; return; }
|
|
@@ -375,6 +397,15 @@ function gravador() {
|
|
|
375
397
|
// celular. Sem esta volta, a conversa de hoje aparece embaixo
|
|
376
398
|
// das de semana passada.
|
|
377
399
|
await salvarConversa({ id, titulo, mensagens: contagem });
|
|
400
|
+
if (nova && batizar) {
|
|
401
|
+
// O título da IA entra depois, num PATCH à parte: se falhar, fica o de antes, sem erro.
|
|
402
|
+
let t = null;
|
|
403
|
+
try { t = await batizar(pergunta, resposta); } catch { /* fica o título de antes */ }
|
|
404
|
+
if (t && t !== titulo) {
|
|
405
|
+
const r = await salvarConversa({ id, titulo: t, mensagens: contagem });
|
|
406
|
+
if (r.ok) titulo = t;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
378
409
|
});
|
|
379
410
|
},
|
|
380
411
|
|
|
@@ -388,7 +419,7 @@ module.exports = {
|
|
|
388
419
|
cifrar, decifrar, cifrarJson, decifrarJson,
|
|
389
420
|
gravador, tituloDe,
|
|
390
421
|
abrir, esquecer,
|
|
391
|
-
salvarConversa, salvarMensagem, listarConversas, lerConversa,
|
|
422
|
+
salvarConversa, salvarMensagem, listarConversas, lerConversa, apagarConversa,
|
|
392
423
|
COLECAO, VERSAO_CIFRA, ITERACOES, TAM_IV,
|
|
393
424
|
// A tradução para o formato do Firestore sai daqui porque é ela que decide
|
|
394
425
|
// o que vira `stringValue` em claro e o que vira `mapValue` cifrado.
|
package/lib/passagem.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* passagem.js — quando o agente PARA sem terminar, ele passa o bastão.
|
|
3
|
+
*
|
|
4
|
+
* Visto no SystemOneHarness (harnessrouter, Apache-2.0; só a ideia, código
|
|
5
|
+
* nosso): ao encerrar sem confiança, o agente grava o estado e o que falta
|
|
6
|
+
* para quem assumir — em vez de só dizer "parei". Aqui, "quem assume" é a
|
|
7
|
+
* próxima conversa ("continua"), que lê o memory.md da pasta.
|
|
8
|
+
*
|
|
9
|
+
* Antes, a memória guardava `Pedido` + os 500 primeiros caracteres do motivo
|
|
10
|
+
* ("Usei as 40 voltas…"). A conversa seguinte sabia QUE parou, mas não o que
|
|
11
|
+
* já existia no disco, o que falhou, nem por onde recomeçar — e relia a pasta
|
|
12
|
+
* às cegas (ou reescrevia um arquivo que já estava pronto).
|
|
13
|
+
*
|
|
14
|
+
* Puro: recebe o registro do pedido (lib/registro.js) e devolve o texto.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const lista = (xs, n = 12) => {
|
|
20
|
+
const v = (xs || []).filter(Boolean);
|
|
21
|
+
return v.length ? v.slice(0, n).join(', ') + (v.length > n ? ` (+${v.length - n})` : '') : '';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{pedido: string, motivo: string, registro?: object, falhasPendentes?: Array<{texto: string}>}} dados
|
|
26
|
+
* @returns {string} bloco curto, em português, para o memory.md
|
|
27
|
+
*/
|
|
28
|
+
function montar({ pedido, motivo, registro, falhasPendentes = [] }) {
|
|
29
|
+
const reg = registro || {};
|
|
30
|
+
const L = [`**Pedido (NÃO concluído):** ${String(pedido || '').replace(/\s+/g, ' ').trim().slice(0, 300)}`,
|
|
31
|
+
`**Por que parei:** ${String(motivo || 'sem explicação').replace(/\s+/g, ' ').trim().slice(0, 300)}`];
|
|
32
|
+
const criados = lista(reg.criados), alterados = lista(reg.alterados);
|
|
33
|
+
if (criados) L.push(`**Já existe no disco (criei):** ${criados}`);
|
|
34
|
+
if (alterados) L.push(`**Já mudei:** ${alterados}`);
|
|
35
|
+
const testes = (reg.testes || []).slice(-3).map((t) => `${t.comando}: ${t.ok ? 'passou' : 'FALHOU'}`);
|
|
36
|
+
if (testes.length) L.push(`**Últimos testes:** ${testes.join(' | ')}`);
|
|
37
|
+
const falhas = [
|
|
38
|
+
...(reg.falhas || []).slice(-3).map((f) => `${f.acao}${f.alvo ? ' ' + f.alvo : ''}: ${f.motivo}`),
|
|
39
|
+
...falhasPendentes.slice(-2).map((f) => f.texto),
|
|
40
|
+
].map((x) => String(x).replace(/\s+/g, ' ').slice(0, 160));
|
|
41
|
+
const unicas = [...new Set(falhas)];
|
|
42
|
+
if (unicas.length) L.push(`**O que falhou:** ${unicas.join(' | ')}`);
|
|
43
|
+
const arquivosFeitos = !!(criados || alterados);
|
|
44
|
+
L.push(arquivosFeitos
|
|
45
|
+
? '**Ao retomar:** NÃO recomece do zero nem reescreva o que já existe — leia os arquivos acima, confira o que falta contra o pedido e continue de onde parou.'
|
|
46
|
+
: '**Ao retomar:** nada foi gravado ainda — comece pelo pedido, mas por um caminho diferente do que travou.');
|
|
47
|
+
return L.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { montar };
|
package/lib/perigo.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
const os = require('os');
|
|
27
27
|
const path = require('path');
|
|
28
|
+
const protecao = require('./protecao');
|
|
28
29
|
|
|
29
30
|
const MODOS = ['auto', 'perguntar', 'livre', 'plano'];
|
|
30
31
|
const COMANDOS = new Set(['run_shell', 'run_in_new_terminal']);
|
|
@@ -122,7 +123,7 @@ function analisarTrecho(palavras, estado) {
|
|
|
122
123
|
if (cmd === 'sudo' || cmd === 'doas') return 'usa sudo: mexe como administrador no computador inteiro';
|
|
123
124
|
if (cmd === 'cd') { estado.cwd = path.resolve(estado.cwd, expandir(alvos[0] || os.homedir())); return null; }
|
|
124
125
|
if ((cmd === 'sh' || cmd === 'bash' || cmd === 'zsh') && args[0] === '-c' && args[1]) {
|
|
125
|
-
return analisarComando(args[1], estado.cwd, estado.projeto);
|
|
126
|
+
return analisarComando(args[1], estado.cwd, estado.projeto, estado.baixados);
|
|
126
127
|
}
|
|
127
128
|
if (cmd === 'rm' || cmd === 'rmdir' || cmd === 'trash') {
|
|
128
129
|
const recursivo = cmd === 'rmdir' || tem(/^-[a-zA-Z]*[rR]|^--recursive$/);
|
|
@@ -141,6 +142,7 @@ function analisarTrecho(palavras, estado) {
|
|
|
141
142
|
}
|
|
142
143
|
if (cmd === 'git') {
|
|
143
144
|
const sub = alvos[0];
|
|
145
|
+
if (sub === 'add') { const r = protecao.gitAddLevaSegredo(alvos, flags, estado.cwd); if (r) return r; }
|
|
144
146
|
if (sub === 'push' && (tem(/^--force$|^-[a-zA-Z]*f/) || alvos.some((a) => /^\+/.test(a)))) {
|
|
145
147
|
return 'git push --force: reescreve o histórico no GitHub, e o que estava lá some';
|
|
146
148
|
}
|
|
@@ -188,7 +190,7 @@ function mexeNoLivro(palavras, cwd) {
|
|
|
188
190
|
return p.slice(1).some((x) => !/^-/.test(x) && noLivro(x.split(/>>?/)[0]));
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
function analisarComando(comando, cwd, projeto) {
|
|
193
|
+
function analisarComando(comando, cwd, projeto, baixados = null) {
|
|
192
194
|
const s = String(comando || '');
|
|
193
195
|
// Baixar e rodar na hora: o que roda é o que o site mandar naquele segundo.
|
|
194
196
|
if (/\b(curl|wget)\b[^;&]*\|\s*(sudo\s+)?(env\s+)?(ba|z|da|k)?sh\b/.test(s)) {
|
|
@@ -196,11 +198,16 @@ function analisarComando(comando, cwd, projeto) {
|
|
|
196
198
|
}
|
|
197
199
|
// O mesmo que mcp_conectar, pela porta dos fundos: não pula o portão.
|
|
198
200
|
if (/\bprimocode\s+mcp\s+(conectar|connect|add)\b/.test(s)) return 'instala e roda um servidor MCP de terceiros';
|
|
199
|
-
const estado = { cwd, projeto, pulandoFlags: false };
|
|
201
|
+
const estado = { cwd, projeto, pulandoFlags: false, baixados };
|
|
200
202
|
for (const t of trechos(s)) {
|
|
201
203
|
if (mexeNoLivro(t.palavras, estado.cwd)) return 'mexe no livro de ações do PrimoCode (~/.primocode/registro), o registro do que o agente fez';
|
|
202
204
|
const r = analisarTrecho(t.palavras, estado);
|
|
203
205
|
if (r) return r;
|
|
206
|
+
const prot = protecao.shellEscreveEmProtegido(t.palavras);
|
|
207
|
+
if (prot) return prot;
|
|
208
|
+
const baix = protecao.rodaBaixado(t.palavras, estado.cwd, baixados);
|
|
209
|
+
if (baix) return baix;
|
|
210
|
+
if (baixados) protecao.baixadosPor(t.palavras).forEach((x) => baixados.add(path.resolve(estado.cwd, x)));
|
|
204
211
|
}
|
|
205
212
|
return null;
|
|
206
213
|
}
|
|
@@ -209,13 +216,13 @@ function analisarComando(comando, cwd, projeto) {
|
|
|
209
216
|
* Esta ação é perigosa?
|
|
210
217
|
* @returns {{perigoso: boolean, motivo: string}}
|
|
211
218
|
*/
|
|
212
|
-
function classificar(nome, args, { projectDir } = {}) {
|
|
219
|
+
function classificar(nome, args, { projectDir, baixados = null } = {}) {
|
|
213
220
|
const a = args || {};
|
|
214
221
|
const projeto = path.resolve(projectDir || process.cwd());
|
|
215
|
-
let motivo = null;
|
|
222
|
+
let motivo = null, fora = false;
|
|
216
223
|
if (COMANDOS.has(nome)) {
|
|
217
224
|
const cwd = a.cwd ? path.resolve(projeto, expandir(a.cwd)) : projeto;
|
|
218
|
-
motivo = analisarComando(a.command, cwd, projeto);
|
|
225
|
+
motivo = analisarComando(a.command, cwd, projeto, baixados);
|
|
219
226
|
} else if (nome === 'mcp_conectar') {
|
|
220
227
|
/* Instala e roda código de terceiro (npx/uvx/docker) com as chaves do
|
|
221
228
|
usuário: sempre pergunta, como o curl | sh. O motivo leva o comando,
|
|
@@ -227,9 +234,10 @@ function classificar(nome, args, { projectDir } = {}) {
|
|
|
227
234
|
const rel = path.relative(projeto, alvo);
|
|
228
235
|
if (rel.startsWith('..') || path.isAbsolute(rel) || /^[a-zA-Z]:[\\/]/.test(String(a.path))) {
|
|
229
236
|
motivo = `mexe num arquivo fora da pasta do projeto (${a.path})`;
|
|
230
|
-
|
|
237
|
+
fora = true;
|
|
238
|
+
} else motivo = protecao.motivoDoArquivo(rel);
|
|
231
239
|
}
|
|
232
|
-
return { perigoso: !!motivo, motivo: motivo || '' };
|
|
240
|
+
return { perigoso: !!motivo, motivo: motivo || '', fora };
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
/* Comando que só olha: todo trecho começa por uma palavra de leitura, sem
|
|
@@ -250,7 +258,7 @@ function somenteLeitura(comando) {
|
|
|
250
258
|
});
|
|
251
259
|
}
|
|
252
260
|
|
|
253
|
-
function novo() { return { liberados: new Set(), negados: new Set() }; }
|
|
261
|
+
function novo() { return { liberados: new Set(), negados: new Set(), baixados: new Set() }; }
|
|
254
262
|
|
|
255
263
|
/* Uma pergunta por vez. Subagentes rodam em paralelo (Promise.all) e dois
|
|
256
264
|
pedidos ao mesmo tempo no mesmo terminal se atropelariam. */
|
|
@@ -274,7 +282,7 @@ async function portao(estado, { modo, nome, args, projectDir, subagent = false,
|
|
|
274
282
|
}
|
|
275
283
|
return { ok: true };
|
|
276
284
|
}
|
|
277
|
-
const { perigoso, motivo: motivoPerigo } = classificar(nome, args, { projectDir });
|
|
285
|
+
const { perigoso, motivo: motivoPerigo, fora } = classificar(nome, args, { projectDir, baixados: estado.baixados });
|
|
278
286
|
const tipo = perigoso ? 'perigo' : (m === 'perguntar' && (COMANDOS.has(nome) || nome === 'mcp_usar')) ? 'comando'
|
|
279
287
|
: (m === 'perguntar' && ESCRITAS.has(nome)) ? 'arquivo' : null;
|
|
280
288
|
if (!tipo) return { ok: true };
|
|
@@ -287,7 +295,7 @@ async function portao(estado, { modo, nome, args, projectDir, subagent = false,
|
|
|
287
295
|
|
|
288
296
|
// Escrever fora da pasta o tools.js já recusa sozinho: perguntar seria
|
|
289
297
|
// pedir um "sim" que não serve para nada.
|
|
290
|
-
if (perigoso && ESCRITAS.has(nome)) {
|
|
298
|
+
if (perigoso && fora && ESCRITAS.has(nome)) {
|
|
291
299
|
return recusa(`Recusado: ${motivo}. O PrimoCode só escreve dentro da pasta do projeto — use /dir se a pessoa quiser trabalhar em outra pasta.`);
|
|
292
300
|
}
|
|
293
301
|
if (subagent) {
|
package/lib/protecao.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* protecao.js — três portas que o perigo.js não tinha, vistas no OpenWorker
|
|
3
|
+
* (andrewyng, MIT) e no repo de skills do Instagram (sergebulaev, MIT).
|
|
4
|
+
* Reescritas do nosso jeito: nenhum código copiado, só a ideia.
|
|
5
|
+
*
|
|
6
|
+
* 1. AUTOPROTEÇÃO. O agente não reescreve, calado, o arquivo que manda nele
|
|
7
|
+
* (AGENTS.md, CLAUDE.md, .primocode/hooks.json…). Quem escreve ali muda o
|
|
8
|
+
* comportamento de TODAS as próximas sessões, e a pessoa nem vê.
|
|
9
|
+
* 2. ARQUIVO QUE EXECUTA DEPOIS. `.git/hooks`, `.github/workflows`,
|
|
10
|
+
* `.vscode/tasks.json`: o texto que entra ali roda mais tarde, fora desta
|
|
11
|
+
* conversa e fora da vista da pessoa.
|
|
12
|
+
* 3. SEGREDO NO GIT. Uma chave real vazou num repo público porque o `.env`
|
|
13
|
+
* foi parar num commit. `git add .env` (ou `git add -A` com um `.env` que
|
|
14
|
+
* o .gitignore não cobre) agora pergunta.
|
|
15
|
+
* 4. PROCEDÊNCIA. Rodar um arquivo que o próprio agente baixou nesta sessão
|
|
16
|
+
* (`curl -o x.sh … && bash x.sh`) sempre pergunta: é o `curl | sh` com um
|
|
17
|
+
* passo no meio.
|
|
18
|
+
*
|
|
19
|
+
* Puro quase todo: o único I/O é ler o .gitignore e ver se o .env existe.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const norm = (p) => String(p || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
28
|
+
|
|
29
|
+
/** Esse caminho (relativo ao projeto) muda o agente ou roda depois? → motivo, ou null. */
|
|
30
|
+
function motivoDoArquivo(rel) {
|
|
31
|
+
const p = norm(rel);
|
|
32
|
+
const nome = path.posix.basename(p);
|
|
33
|
+
if (/^(agents|claude|gemini)\.md$/i.test(nome) || nome === '.cursorrules' || /(^|\/)\.github\/copilot-instructions\.md$/.test(p)
|
|
34
|
+
|| /(^|\/)\.(claude|cursor)\/(rules|commands|agents|settings(\.local)?\.json|hooks)/.test(p)
|
|
35
|
+
|| /(^|\/)\.primocode\/(hooks\.json|comandos\/)/.test(p)) {
|
|
36
|
+
return `mexe num arquivo de regras do agente (${nome}): o que entra ali vale para todas as próximas sessões`;
|
|
37
|
+
}
|
|
38
|
+
if (/(^|\/)\.git\/hooks\//.test(p)) return `escreve num gancho do git (${nome}): roda sozinho em todo commit`;
|
|
39
|
+
if (/(^|\/)\.github\/workflows\//.test(p)) return `escreve num fluxo do GitHub Actions (${nome}): roda nos servidores, com as chaves do repositório`;
|
|
40
|
+
if (/(^|\/)\.vscode\/tasks\.json$/.test(p)) return 'escreve em .vscode/tasks.json: o editor roda isso quando a pasta é aberta';
|
|
41
|
+
if (/(^|\/)\.husky\//.test(p)) return `escreve num gancho do husky (${nome}): roda sozinho em todo commit`;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Palavra de comando que aponta para arquivo protegido? Tira o `>`/`>>` do começo. */
|
|
46
|
+
function arquivoNaPalavra(w) {
|
|
47
|
+
const s = String(w).replace(/^\d?>>?/, '');
|
|
48
|
+
return s ? motivoDoArquivo(s) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ESCREVEM = new Set(['tee', 'cp', 'mv', 'ln', 'install', 'touch', 'truncate', 'rm', 'rsync', 'dd']);
|
|
52
|
+
/** Um trecho de shell (já em palavras) escreve num arquivo protegido? */
|
|
53
|
+
function shellEscreveEmProtegido(palavras) {
|
|
54
|
+
if (!palavras.length) return null;
|
|
55
|
+
const cmd = path.basename(palavras[0]);
|
|
56
|
+
const redirecionou = palavras.some((w, i) => /^\d?>>?$/.test(w) ? !!(palavras[i + 1] && motivoDoArquivo(palavras[i + 1])) : /^\d?>>?./.test(w) && !!arquivoNaPalavra(w));
|
|
57
|
+
const sedI = cmd === 'sed' && palavras.some((w) => /^-[a-zA-Z]*i/.test(w) || w === '--in-place');
|
|
58
|
+
if (!redirecionou && !sedI && !ESCREVEM.has(cmd)) return null;
|
|
59
|
+
for (let i = 0; i < palavras.length; i++) {
|
|
60
|
+
const w = palavras[i];
|
|
61
|
+
const alvo = /^\d?>>?$/.test(w) ? palavras[i + 1] : w;
|
|
62
|
+
const m = alvo && (/^\d?>>?./.test(alvo) ? arquivoNaPalavra(alvo) : (i > 0 || /^\d?>>?$/.test(w)) ? motivoDoArquivo(alvo) : null);
|
|
63
|
+
if (m) return m;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ── segredo no git ─────────────────────────────────────────────────── */
|
|
69
|
+
|
|
70
|
+
const MODELO = /\.(example|sample|template|dist|defaults?)$/i;
|
|
71
|
+
function nomeDeSegredo(alvo) {
|
|
72
|
+
const nome = path.posix.basename(norm(alvo));
|
|
73
|
+
if (MODELO.test(nome)) return false;
|
|
74
|
+
return /^\.env(\.[\w-]+)?$/i.test(nome) || /^id_(rsa|ed25519|ecdsa|dsa)$/.test(nome) || /\.(pem|p12|pfx|key)$/i.test(nome)
|
|
75
|
+
|| /^(credentials?|service-account.*|secrets?)\.(json|ya?ml)$/i.test(nome) || nome === '.npmrc' || nome === '.netrc';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function envFicaDeFora(projeto) {
|
|
79
|
+
try {
|
|
80
|
+
const gi = fs.readFileSync(path.join(projeto, '.gitignore'), 'utf8').split('\n').map((l) => l.trim());
|
|
81
|
+
return gi.some((l) => /^\/?\.env(\*|\.\*)?\/?$/.test(l) || l === '*.env');
|
|
82
|
+
} catch { return false; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `git add …` leva um segredo? `alvos` já sem as flags. */
|
|
86
|
+
function gitAddLevaSegredo(alvos, flags, projeto) {
|
|
87
|
+
const lista = alvos.slice(1);
|
|
88
|
+
const duro = lista.find(nomeDeSegredo);
|
|
89
|
+
if (duro) return `git add ${duro}: um arquivo de segredo no commit vira chave vazada no GitHub`;
|
|
90
|
+
const tudo = lista.some((a) => a === '.' || a === '*' || a === ':/') || flags.some((f) => /^(-A|--all|-a)$/.test(f));
|
|
91
|
+
if (tudo && projeto) {
|
|
92
|
+
const temEnv = ['.env', '.env.local', '.env.production'].some((n) => { try { return fs.existsSync(path.join(projeto, n)); } catch { return false; } });
|
|
93
|
+
if (temEnv && !envFicaDeFora(projeto)) return 'git add de tudo, com um .env na pasta que o .gitignore não cobre: a chave iria junto no commit';
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ── procedência: o que o agente baixou ─────────────────────────────── */
|
|
99
|
+
|
|
100
|
+
/** Arquivos que um trecho de shell baixa (curl -o/-O, wget -O/padrão). */
|
|
101
|
+
function baixadosPor(palavras) {
|
|
102
|
+
const cmd = path.basename(palavras[0] || '');
|
|
103
|
+
const urls = palavras.filter((w) => /^https?:\/\//i.test(w));
|
|
104
|
+
const saida = [];
|
|
105
|
+
if (cmd === 'curl') {
|
|
106
|
+
for (let i = 1; i < palavras.length; i++) {
|
|
107
|
+
const w = palavras[i];
|
|
108
|
+
if ((w === '-o' || w === '--output') && palavras[i + 1]) saida.push(palavras[i + 1]);
|
|
109
|
+
else if (/^-[a-zA-Z]*O$/.test(w) || w === '--remote-name') urls.forEach((u) => saida.push(path.posix.basename(u.split(/[?#]/)[0])));
|
|
110
|
+
else if (/^-o./.test(w)) saida.push(w.slice(2));
|
|
111
|
+
}
|
|
112
|
+
} else if (cmd === 'wget') {
|
|
113
|
+
const o = palavras.findIndex((w) => w === '-O' || w === '--output-document');
|
|
114
|
+
if (o > 0 && palavras[o + 1]) saida.push(palavras[o + 1]);
|
|
115
|
+
else urls.forEach((u) => saida.push(path.posix.basename(u.split(/[?#]/)[0]) || 'index.html'));
|
|
116
|
+
}
|
|
117
|
+
return saida.filter((x) => x && x !== '-');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const INTERPRETES = new Set(['sh', 'bash', 'zsh', 'dash', 'node', 'python', 'python3', 'ruby', 'perl', 'source', '.', 'deno', 'bun', 'npx', 'tsx']);
|
|
121
|
+
/** O trecho roda um arquivo que foi baixado? `baixados` = Set de caminhos absolutos. */
|
|
122
|
+
function rodaBaixado(palavras, cwd, baixados) {
|
|
123
|
+
if (!baixados || !baixados.size || !palavras.length) return null;
|
|
124
|
+
const abs = (x) => path.resolve(cwd, String(x));
|
|
125
|
+
const cmd = palavras[0];
|
|
126
|
+
let alvo = null;
|
|
127
|
+
if (INTERPRETES.has(path.basename(cmd))) alvo = palavras.slice(1).find((w) => !/^-/.test(w));
|
|
128
|
+
else if (/^\.{0,2}\//.test(cmd) || cmd.includes('/')) alvo = cmd;
|
|
129
|
+
else if (baixados.has(abs(cmd))) alvo = cmd;
|
|
130
|
+
if (alvo && baixados.has(abs(alvo))) return `roda um arquivo que o próprio agente baixou nesta sessão (${path.basename(alvo)}) sem a pessoa ter lido`;
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { motivoDoArquivo, shellEscreveEmProtegido, nomeDeSegredo, gitAddLevaSegredo, envFicaDeFora, baixadosPor, rodaBaixado };
|
package/lib/repeticao.js
CHANGED
|
@@ -100,7 +100,7 @@ function consultaDe(nome, args) {
|
|
|
100
100
|
* ficam fora da época. */
|
|
101
101
|
const OBSERVAM = new Set(['read_file', 'list_dir', 'glob', 'grep', 'browser_read', 'browser_screenshot',
|
|
102
102
|
'desktop_screenshot', 'desktop_size', 'desktop_windows', 'desktop_elements']);
|
|
103
|
-
const NAO_MUDAM = new Set(['write_todos', 'remember_general', 'pesquisar_referencia', ...REFERENCIAS]);
|
|
103
|
+
const NAO_MUDAM = new Set(['write_todos', 'remember_general', 'lembrar', 'pesquisar_referencia', ...REFERENCIAS]);
|
|
104
104
|
const SHELL_SO_LEITURA = /^\s*(ls|cat|head|tail|pwd|echo|grep|rg|find|wc|tree|stat|file|which|type|du|df|env|printenv|date|git\s+(status|log|diff|show|branch|remote))\b[^;&|>]*$/;
|
|
105
105
|
|
|
106
106
|
function comandoDe(args) {
|