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.
Files changed (245) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +74 -9
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +38 -8
  198. package/lib/app-chat.js +97 -0
  199. package/lib/catalogo.js +7 -1
  200. package/lib/chamada-torta.js +81 -0
  201. package/lib/editor-pro.js +786 -0
  202. package/lib/entrega.js +58 -0
  203. package/lib/estudio/conferencia-visual.js +76 -0
  204. package/lib/estudio/diretor.js +6 -1
  205. package/lib/estudio/exportar.js +3 -1
  206. package/lib/estudio/gosto/tela.html +1349 -0
  207. package/lib/estudio/gosto.js +305 -0
  208. package/lib/estudio/index.js +12 -1
  209. package/lib/estudio/kit-ae/base.jsx +119 -13
  210. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  211. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  212. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  213. package/lib/estudio/maquina-diretor.js +70 -12
  214. package/lib/estudio/maquina-foto.js +177 -0
  215. package/lib/estudio/maquina-guarda.js +207 -0
  216. package/lib/estudio/maquina-layout.js +22 -1
  217. package/lib/estudio/maquina.js +64 -11
  218. package/lib/estudio/referencias.js +1 -1
  219. package/lib/estudio/visao/visao.swift +97 -0
  220. package/lib/estudio/visao.js +60 -0
  221. package/lib/headless.js +15 -3
  222. package/lib/local/cerebro.json +285 -7
  223. package/lib/local/motor.js +5 -2
  224. package/lib/memoria-usuario.js +164 -0
  225. package/lib/nuvem.js +33 -2
  226. package/lib/passagem.js +50 -0
  227. package/lib/perigo.js +19 -11
  228. package/lib/protecao.js +134 -0
  229. package/lib/repeticao.js +1 -1
  230. package/lib/sandbox.js +57 -9
  231. package/lib/titulo-conversa.js +46 -0
  232. package/lib/tools.js +23 -4
  233. package/lib/ui.js +2 -2
  234. package/package.json +3 -2
  235. package/skills/editar-meu-video/SKILL.md +6 -2
  236. package/skills/editar-video/SKILL.md +113 -0
  237. package/studio/pessoa.py +52 -0
  238. package/voz/primo_voz/__main__.py +55 -40
  239. package/voz/primo_voz/app.py +500 -4
  240. package/voz/primo_voz/chat.html +1773 -0
  241. package/voz/primo_voz/conversa.py +0 -26
  242. package/voz/primo_voz/hud.html +8 -2
  243. package/voz/primo_voz/hud.py +129 -5
  244. package/voz/primo_voz/identidade.py +1 -1
  245. 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.
@@ -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) {
@@ -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) {