primocode 10.16.0 → 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 +11 -2
- 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 +36 -8
- package/lib/catalogo.js +4 -0
- 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.js +45 -4
- package/lib/estudio/index.js +8 -5
- 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 +61 -7
- 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 +60 -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/local/cerebro.json +192 -6
- package/lib/local/motor.js +5 -2
- package/lib/nuvem.js +11 -1
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +3 -0
- package/lib/ui.js +1 -1
- 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/app.py +43 -4
- package/voz/primo_voz/chat.html +17 -6
- package/voz/primo_voz/painel_dados.py +134 -0
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 };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* titulo-conversa.js — quem dá título à conversa é quem leu a primeira troca.
|
|
3
|
+
*
|
|
4
|
+
* Antes o título era o começo da primeira mensagem ("Oi.", "E ele tem o…"): o
|
|
5
|
+
* gravador (lib/nuvem.js) usava `tituloDe`. Agora, depois da primeira resposta
|
|
6
|
+
* de uma conversa NOVA, o modelo escreve um título de 2 a 5 palavras e o
|
|
7
|
+
* gravador troca o do documento na nuvem. O app (Mac e desktop) lê esse título
|
|
8
|
+
* em `primocode conversas --json`, então a lateral muda sozinha.
|
|
9
|
+
*
|
|
10
|
+
* Se o modelo não responder a tempo, ou vier lixo, vale o título de antes:
|
|
11
|
+
* nenhum erro aparece. Puro, menos `escolher`, que recebe quem pergunta de fora.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const TEMPO_MAXIMO_MS = 8000;
|
|
16
|
+
|
|
17
|
+
/** A pergunta. Curta: vai numa chamada à parte. */
|
|
18
|
+
function pergunta(pedido, resposta) {
|
|
19
|
+
return 'Dê um título curto para esta conversa, de 2 a 5 palavras, em português do Brasil. '
|
|
20
|
+
+ 'O título diz o ASSUNTO (ex.: Site da padaria, Vídeo de lançamento, Erro no login), '
|
|
21
|
+
+ 'nunca "Conversa" nem "Oi". Sem aspas, sem ponto final e sem explicação. Responda SÓ com o título.\n\n'
|
|
22
|
+
+ `PESSOA: ${String(pedido || '').slice(0, 500)}\n\nPRIMO: ${String(resposta || '').slice(0, 500)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Limpa a resposta do modelo. Devolve null quando não dá pra confiar nela. */
|
|
26
|
+
function limpar(resposta) {
|
|
27
|
+
const linha = String(resposta || '').trim().split('\n').map((l) => l.trim()).find(Boolean) || '';
|
|
28
|
+
const t = linha.replace(/^(t[ií]tulo)\s*:\s*/i, '').replace(/^[`"'“”*#\s]+|[`"'“”*.\s]+$/g, '').replace(/\s+/g, ' ');
|
|
29
|
+
const palavras = t.split(' ').filter(Boolean);
|
|
30
|
+
if (!t || t.length > 60 || palavras.length < 1 || palavras.length > 7) return null;
|
|
31
|
+
if (/^(conversa|oi|ol[aá]|t[ií]tulo|sem t[ií]tulo)$/i.test(t)) return null;
|
|
32
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pergunta ao modelo e devolve o título limpo, ou null (falhou, demorou ou veio lixo). Nunca rejeita. */
|
|
36
|
+
async function escolher({ pedido, resposta, perguntar, tempoMs = TEMPO_MAXIMO_MS }) {
|
|
37
|
+
try {
|
|
38
|
+
let timer;
|
|
39
|
+
const limite = new Promise((ok) => { timer = setTimeout(() => ok(null), tempoMs); });
|
|
40
|
+
const r = await Promise.race([Promise.resolve(perguntar(pergunta(pedido, resposta))), limite]);
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
return r == null ? null : limpar(r);
|
|
43
|
+
} catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { pergunta, limpar, escolher, TEMPO_MAXIMO_MS };
|
package/lib/tools.js
CHANGED
|
@@ -862,6 +862,9 @@ async function despachar(name, args, ctx) {
|
|
|
862
862
|
// A transcrição precisa do servidor (a chave é dele), então o
|
|
863
863
|
// contexto do pedido atravessa até aqui.
|
|
864
864
|
return estudio.ferramentaVideo(args, ctx);
|
|
865
|
+
case 'editor_pro':
|
|
866
|
+
// A edição completa de vídeo FALADO: o motor do EDIT IA PRO (lib/editor-pro.js, editor/).
|
|
867
|
+
return require('./editor-pro.js').ferramenta(args, ctx);
|
|
865
868
|
case 'estudio_render':
|
|
866
869
|
// A pasta de trabalho vai junto: a composição do Hyperframes que a IA escreveu mora nela.
|
|
867
870
|
return estudio.ferramentaRender({ ...args, __pasta: projectDir }, {
|
package/lib/ui.js
CHANGED
|
@@ -552,7 +552,7 @@ const ROTULOS = {
|
|
|
552
552
|
legítimo — e só a linha de baixo revelava que tinha falhado. */
|
|
553
553
|
planejar_producao:'Diretor', estudio_projeto:'Estúdio·projeto',
|
|
554
554
|
estudio_plano:'Estúdio·plano', estudio_narrar:'Estúdio·narração',
|
|
555
|
-
estudio_video:'Estúdio·vídeo', estudio_render:'Estúdio·render',
|
|
555
|
+
estudio_video:'Estúdio·vídeo', estudio_render:'Estúdio·render', editor_pro:'Editor·vídeo',
|
|
556
556
|
estudio_site:'Estúdio·marca',
|
|
557
557
|
desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
|
|
558
558
|
desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.17.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
11
|
"postinstall": "node lib/preparar.js || true",
|
|
12
12
|
"test:server": "cd server && npm test",
|
|
13
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/primo-voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js && node test/app-chat-cli.test.js && node test/app-chat-ponte.test.js && node test/gosto-motion.test.js && node test/desktop-vivo.test.js && node test/memoria-usuario.test.js && node test/landing-instalar.test.js"
|
|
13
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/primo-voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js && node test/app-chat-cli.test.js && node test/app-chat-ponte.test.js && node test/gosto-motion.test.js && node test/desktop-vivo.test.js && node test/desktop-chat.test.js && node test/desktop-voz.test.js && node test/desktop-painel-dados.test.js && node test/titulo-conversa.test.js && node test/memoria-usuario.test.js && node test/landing-instalar.test.js && node test/foto-na-cena.test.js && node test/protecao.test.js && node test/entrega.test.js && node test/chamada-torta.test.js && node test/passagem.test.js && node test/editor-pro.test.js && node test/gosto-app.test.js && node test/guarda-layout.test.js && node test/acabamento.test.js"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18.17.0"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"lib/",
|
|
32
32
|
"studio/",
|
|
33
33
|
"skills/",
|
|
34
|
+
"editor/",
|
|
34
35
|
"voz/",
|
|
35
36
|
"LICENSES/",
|
|
36
37
|
"README.md",
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
nome: Editar o vídeo gravado
|
|
3
|
-
descricao:
|
|
4
|
-
quando:
|
|
3
|
+
descricao: operações soltas numa gravação (importar, cortar silêncio, recortar a pessoa) — a edição completa é a skill editar-video
|
|
4
|
+
quando: cortar silêncio, cortar silencio, recortar a pessoa, trocar o fundo, b-roll, broll, importar a gravação
|
|
5
5
|
autor: PrimoCode
|
|
6
6
|
---
|
|
7
|
+
A EDIÇÃO COMPLETA de um vídeo falado (corte, legenda animada, gancho, peças,
|
|
8
|
+
sons, trilha) é do `editor_pro` — carregue a skill `editar-video`. Esta aqui é
|
|
9
|
+
para as operações soltas no Estúdio.
|
|
10
|
+
|
|
7
11
|
O vídeo é da pessoa. Ela apareceu, falou e errou — o seu trabalho é tirar o
|
|
8
12
|
que atrapalha, não refazer o que ela fez.
|
|
9
13
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
nome: Editar vídeo falado (motor EDIT IA PRO)
|
|
3
|
+
descricao: a edição completa da gravação da pessoa falando — corte, voz, legenda, gancho, peças, b-roll, sons, trilha e portão
|
|
4
|
+
quando: edita meu vídeo, editar meu vídeo, edite esse vídeo, editar o vídeo, edição do vídeo, edicao do video, reels, tiktok, shorts, criativo, legenda e efeitos, legendas, gancho, cortar os respiros, tirar os erros, melhor tomada, vídeo falado, gravei, gravação, gravacao, vsl, anúncio
|
|
5
|
+
autor: PrimoCode (adaptada do EDIT IA PRO, SHK Soluções Digitais, com autorização)
|
|
6
|
+
---
|
|
7
|
+
Você é o editor e o diretor. O motor (ferramenta `editor_pro`, ou
|
|
8
|
+
`primocode editor <acao> --json '{...}'` pelo run_shell) mede e faz o
|
|
9
|
+
trabalho pesado; você decide o que aparece em cada momento e por quê.
|
|
10
|
+
|
|
11
|
+
## Como falar com a pessoa
|
|
12
|
+
|
|
13
|
+
- Português simples, frases curtas. Nada de JSON, comando ou caminho comprido
|
|
14
|
+
na conversa: "estou ouvindo as tomadas", "tirei os respiros".
|
|
15
|
+
- Tempos no formato 00:12. Antes de passo demorado (transcrição, render),
|
|
16
|
+
diga mais ou menos quanto leva.
|
|
17
|
+
- No máximo UMA pergunta, e só se não der para deduzir ("é anúncio, Reels ou
|
|
18
|
+
aula?"). O resto tem padrão; conte as suposições numa frase no fim.
|
|
19
|
+
|
|
20
|
+
## Princípios
|
|
21
|
+
|
|
22
|
+
1. **A fala gravada manda.** Tudo entra no tempo da palavra que chama (pode
|
|
23
|
+
antecipar até 0,15 s). Tempo vem da fala que o `cortar` devolve, nunca
|
|
24
|
+
de estimativa.
|
|
25
|
+
2. **Fidelidade.** Número, nome, prova e oferta só se a pessoa disse. Faltou
|
|
26
|
+
material: diga o que falta, não invente.
|
|
27
|
+
3. **Respiro é erro.** O corte já tira silêncio de 0,25 s ou mais e fica com a
|
|
28
|
+
última tomada inteira de cada frase.
|
|
29
|
+
4. **Uma ideia por vez na tela; rosto livre.** Nada sobre olhos e boca.
|
|
30
|
+
Alguma coisa muda a cada 2 a 4 s.
|
|
31
|
+
5. **Só entrega com o portão PASSOU.** Reprovou: conserte o plano e monte de
|
|
32
|
+
novo. Nunca mostre um vídeo reprovado.
|
|
33
|
+
|
|
34
|
+
## O caminho
|
|
35
|
+
|
|
36
|
+
1. `editor_pro acao:"cortar" video:"<arquivo>" prompt:"nomes, de, marca"`.
|
|
37
|
+
Na primeira vez ele baixa os arquivos do editor e cria o Python dele
|
|
38
|
+
(avise: alguns minutos). Volta `pasta`, `duracao`, `volume` e `fala`: as
|
|
39
|
+
frases do corte com o tempo (`3.42-5.10 texto`). O original nunca é tocado.
|
|
40
|
+
2. **Leia a fala e escreva o plano.** Divida por ideia e decida o que entra
|
|
41
|
+
em cada bloco (tabela "que peça para cada fala", abaixo).
|
|
42
|
+
3. `editor_pro acao:"montar" pasta:"<pasta>" plano:{...} trilha:"auto"`.
|
|
43
|
+
Ele cria o projeto, põe a câmera viva, os sons casados com cada animação,
|
|
44
|
+
renderiza, mixa a trilha com ducking e roda o portão. Volta `video`
|
|
45
|
+
(`<pasta>/final.mp4`), `quadros` (6 PNGs), `audio` e `portao`.
|
|
46
|
+
4. **Olhe os quadros** (read_file nos PNGs) antes de entregar. Algo feio
|
|
47
|
+
(texto cortado, peça no rosto, b-roll sem nada a ver): mude o plano e
|
|
48
|
+
monte de novo.
|
|
49
|
+
5. Entregue com uma nota curta: onde está o vídeo, o que foi feito, o que
|
|
50
|
+
faltou. Atalho sem plano: `acao:"editar"` (o plano automático põe um
|
|
51
|
+
gancho e as peças que a fala pede).
|
|
52
|
+
|
|
53
|
+
## O plano
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{"gancho": "O que é *IA*",
|
|
57
|
+
"pecas": [
|
|
58
|
+
{"id": "def", "tipo": "lettering", "principal": "Máquinas *aprendem*", "apoio": "e decidem", "em": 6.9, "sai": 10.6, "posicao": "topo"},
|
|
59
|
+
{"id": "passos", "tipo": "passo_a_passo", "depois_de": 12.3, "sai": 23.1,
|
|
60
|
+
"passos": [{"texto": "Recebe exemplos", "em": 12.6}, {"texto": "Ajusta parâmetros", "em": 16.9}]},
|
|
61
|
+
{"id": "lista", "tipo": "lista_check", "titulo": "Tudo isso é *IA*", "marca": "check", "depois_de": 31, "sai": 35,
|
|
62
|
+
"itens": [{"texto": "Assistentes", "em": 31.05}, {"texto": "Carros sozinhos", "em": 32.9}]}],
|
|
63
|
+
"broll": [{"busca": "self-driving car", "de": 26.1, "ate": 28.4, "rotulo": "Carro *autônomo*"}]}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- `gancho`: até 4 palavras fortes da primeira frase; `*palavra*` sai em
|
|
67
|
+
destaque. `false` desliga.
|
|
68
|
+
- `pecas`: as do catálogo. `em`, `sai`, `depois_de` em segundos do corte.
|
|
69
|
+
- `broll`: imagem livre (Wikimedia, CC0) buscada pelo que a pessoa DISSE
|
|
70
|
+
naquele instante (busca em inglês acha mais), 1,5 a 3 s, com `rotulo`
|
|
71
|
+
curto. `"modo": "tela_verde"` recorta a pessoa na frente da imagem.
|
|
72
|
+
- Opcionais: `estilo` (`laranja` padrão, `papelaria`, `noite`), `legenda`
|
|
73
|
+
(`padrao` | `discreta`), `correcoes` (`{"palavra errada": "certa"}` quando a
|
|
74
|
+
transcrição errou um nome), `blocos` (enquadramento à mão), `trilha_abaixo`
|
|
75
|
+
(dB da trilha abaixo da voz, 20), `sons: false`, `camera_viva: false`.
|
|
76
|
+
- `trilha`: `"auto"` (busca uma faixa com energia), o caminho de um áudio da
|
|
77
|
+
pessoa, ou `"nenhuma"`.
|
|
78
|
+
|
|
79
|
+
## Que peça para cada fala
|
|
80
|
+
|
|
81
|
+
| a fala | a peça |
|
|
82
|
+
|---|---|
|
|
83
|
+
| uma expressão que muda o sentido | `lettering` (`principal`, `apoio`) |
|
|
84
|
+
| enumera ("espaço, estoque, frete") | `lista_check` (`marca` check ou x), cada item na sua palavra, até 22 letras |
|
|
85
|
+
| etapas em ordem | `passo_a_passo` |
|
|
86
|
+
| um número que importa | `contador` (`valor_final`, `sufixo`, `rotulo`) |
|
|
87
|
+
| nome de uma coisa a fixar | `rotulo` (em cima da cabeça) |
|
|
88
|
+
| afirmação curta que fecha a ideia | `carimbo` (1 ou 2 palavras) |
|
|
89
|
+
| cita algo concreto que se pode mostrar | `broll` |
|
|
90
|
+
|
|
91
|
+
Peça grande (lista, passos) ganha moldura sozinha. Lettering com mais de 6
|
|
92
|
+
palavras é erro. Campos de cada peça: `<editor>/references/animacao-catalogo.md`;
|
|
93
|
+
enquadramento: `references/enquadramento.md`; sons e trilha:
|
|
94
|
+
`references/som-e-trilha.md` (`<editor>` é a pasta que o `preparar` devolve).
|
|
95
|
+
|
|
96
|
+
## O som
|
|
97
|
+
|
|
98
|
+
O motor mede a voz de cada trecho e acerta o ganho antes do volume final:
|
|
99
|
+
gravação baixa (-38 LUFS) sai em -14 LUFS, pico até -1 dBTP, 48 kHz, sem o
|
|
100
|
+
modo dinâmico que bombeia a voz. A limpeza de ruído é leve por padrão
|
|
101
|
+
(`limpar: "forte"` no cortar, `false` desliga). O portão reprova volume fora,
|
|
102
|
+
modo dinâmico, amostra clipada, respiro, quadro preto e texto fora da zona
|
|
103
|
+
segura do Reels.
|
|
104
|
+
|
|
105
|
+
## Quando algo dá errado
|
|
106
|
+
|
|
107
|
+
- Erro de um passo: a mensagem diz qual e por quê (em português). Corrija a
|
|
108
|
+
entrada e tente de novo; não repita o mesmo pedido esperando outro
|
|
109
|
+
resultado.
|
|
110
|
+
- Palavra errada na legenda: `correcoes` no plano, ou `prompt` no cortar.
|
|
111
|
+
- A mesma correção falhou duas vezes: pare e conte à pessoa o que testou.
|
|
112
|
+
- Operação solta (só cortar um trecho, juntar, exportar): é o editor por
|
|
113
|
+
linha do tempo (`primocode video help`), não este.
|
package/studio/pessoa.py
CHANGED
|
@@ -175,6 +175,42 @@ def recortar(entrada, saida, suavizar=7, encolher=2, limiar=0.6):
|
|
|
175
175
|
"largura": largura, "altura": altura, "fps": round(fps, 2)}
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
def recortar_foto(entrada, saida, suavizar=7, encolher=2, limiar=0.6, minimo=0.08):
|
|
179
|
+
"""Recorta a pessoa de UMA FOTO e grava um PNG com alfa (foto na cena do motion).
|
|
180
|
+
|
|
181
|
+
Se a pessoa ocupa menos de `minimo` do quadro (ou não há pessoa), devolve
|
|
182
|
+
semPessoa: quem chamou usa a foto inteira com moldura arredondada.
|
|
183
|
+
"""
|
|
184
|
+
import cv2
|
|
185
|
+
import numpy as np
|
|
186
|
+
import mediapipe as mp
|
|
187
|
+
|
|
188
|
+
quadro = cv2.imread(entrada, cv2.IMREAD_COLOR)
|
|
189
|
+
if quadro is None:
|
|
190
|
+
return {"ok": False, "error": f"não consegui abrir a foto {entrada}"}
|
|
191
|
+
segmentador = mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection=0)
|
|
192
|
+
try:
|
|
193
|
+
resultado = segmentador.process(cv2.cvtColor(quadro, cv2.COLOR_BGR2RGB))
|
|
194
|
+
finally:
|
|
195
|
+
segmentador.close()
|
|
196
|
+
mascara = (resultado.segmentation_mask > limiar).astype(np.uint8) * 255
|
|
197
|
+
cobertura = float((mascara > 0).mean())
|
|
198
|
+
if cobertura < minimo:
|
|
199
|
+
return {"ok": False, "semPessoa": True, "cobertura": round(cobertura, 3),
|
|
200
|
+
"error": "não achei uma pessoa nesta foto."}
|
|
201
|
+
if encolher > 0:
|
|
202
|
+
mascara = cv2.erode(mascara, np.ones((3, 3), np.uint8), iterations=int(encolher))
|
|
203
|
+
if suavizar > 0:
|
|
204
|
+
k = int(suavizar) | 1
|
|
205
|
+
mascara = cv2.GaussianBlur(mascara, (k, k), 0)
|
|
206
|
+
bgra = cv2.cvtColor(quadro, cv2.COLOR_BGR2BGRA)
|
|
207
|
+
bgra[:, :, 3] = mascara
|
|
208
|
+
if not cv2.imwrite(saida, bgra):
|
|
209
|
+
return {"ok": False, "error": f"não consegui gravar o recorte em {saida}"}
|
|
210
|
+
return {"ok": True, "arquivo": saida, "cobertura": round(cobertura, 3),
|
|
211
|
+
"largura": int(quadro.shape[1]), "altura": int(quadro.shape[0])}
|
|
212
|
+
|
|
213
|
+
|
|
178
214
|
def main():
|
|
179
215
|
p = argparse.ArgumentParser(description="Recorte de pessoa do PrimoCode Studio")
|
|
180
216
|
sub = p.add_subparsers(dest="comando", required=True)
|
|
@@ -185,10 +221,26 @@ def main():
|
|
|
185
221
|
r.add_argument("--suavizar", type=int, default=7)
|
|
186
222
|
r.add_argument("--encolher", type=int, default=2)
|
|
187
223
|
r.add_argument("--limiar", type=float, default=0.6)
|
|
224
|
+
f = sub.add_parser("recortar-foto")
|
|
225
|
+
f.add_argument("--entrada", required=True)
|
|
226
|
+
f.add_argument("--saida", required=True)
|
|
227
|
+
f.add_argument("--suavizar", type=int, default=7)
|
|
228
|
+
f.add_argument("--encolher", type=int, default=2)
|
|
229
|
+
f.add_argument("--limiar", type=float, default=0.6)
|
|
188
230
|
args = p.parse_args()
|
|
189
231
|
|
|
190
232
|
falta = faltando()
|
|
191
233
|
|
|
234
|
+
if args.comando == "recortar-foto":
|
|
235
|
+
# Dentro do motion não se instala nada (o MediaPipe passa de 100 MB): sem ele, a foto vai inteira, com moldura.
|
|
236
|
+
if falta:
|
|
237
|
+
saida = {"ok": False, "semMediaPipe": True, "falta": falta,
|
|
238
|
+
"error": "o recorte de pessoa precisa de " + ", ".join(falta) + " (a foto segue inteira, com moldura)."}
|
|
239
|
+
else:
|
|
240
|
+
saida = recortar_foto(args.entrada, args.saida, args.suavizar, args.encolher, args.limiar)
|
|
241
|
+
print(json.dumps(saida, ensure_ascii=False))
|
|
242
|
+
sys.exit(0 if saida.get("ok") else 1)
|
|
243
|
+
|
|
192
244
|
if args.comando == "conferir":
|
|
193
245
|
saida = {"ok": not falta, "falta": falta,
|
|
194
246
|
"comoResolver": (f"{sys.executable} -m pip install --user " + " ".join(falta)
|
package/voz/primo_voz/app.py
CHANGED
|
@@ -86,6 +86,33 @@ def achar_projeto(nome: str) -> "Path | None":
|
|
|
86
86
|
|
|
87
87
|
PEDIDO_PASTA = re.compile(r"^(?:(?:abre|abrir|abra|troca|trocar|muda|mudar|vai|ir)\s+(?:para|pra|pro|no|na|o|a)?\s*(?:o\s+|a\s+)?(?:projeto|pasta)\s+|trabalha(?:r)?\s+(?:no|na|em)\s+(?:o\s+|a\s+)?(?:projeto\s+|pasta\s+)?)(.+?)[.!?]*$", re.I)
|
|
88
88
|
VERBO_ACAO = re.compile(r"^(?:e\s+|ent[aã]o\s+|agora\s+)?(?:cria|crie|criar|faz|fa[cç]a|fazer|monta|monte|gera|gere|escreve|escreva|edita|edite|muda|mude|corrige|corrija|conserta|conserte|instala|instale|roda|rode|executa|execute|abre|abra|apaga|apague|deleta|enxuga|enxugue|atualiza|atualize|adiciona|adicione|remove|remova|refatora|publica|publique|coloca|ponha|p[oõ]e|troca|continua|continue|pode ser|sim|isso|ok|beleza|manda|bora|vai)\b", re.I)
|
|
89
|
+
# O 1º motion sem gosto salvo: o motor (em -p, PRIMOCODE_APP=1) escreve no stderr `primocode:abrir-gosto <url>` e espera.
|
|
90
|
+
# O app bloqueia navegador por padrão (PRIMOCODE_SEM_NAVEGADOR=1); ESTA é a exceção explícita: a tela local do gosto abre
|
|
91
|
+
# no navegador padrão. Só a tela local (127.0.0.1 + token hex) passa; qualquer outra coisa na linha é recusada.
|
|
92
|
+
GOSTO_LINHA = re.compile(r"^primocode:abrir-gosto (http://127\.0\.0\.1:\d{2,5}/\?t=[0-9a-f]{32})$")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def url_gosto(linha: str) -> "str | None":
|
|
96
|
+
m = GOSTO_LINHA.match(str(linha or "").strip())
|
|
97
|
+
return m.group(1) if m else None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def abrir_gosto(linha: str, abrir=None) -> bool:
|
|
101
|
+
"""True se a linha era do gosto (aberta ou recusada); False se é outra linha qualquer."""
|
|
102
|
+
if not str(linha or "").startswith("primocode:abrir-gosto"):
|
|
103
|
+
return False
|
|
104
|
+
url = url_gosto(linha)
|
|
105
|
+
if not url:
|
|
106
|
+
log("gosto: linha recusada")
|
|
107
|
+
return True
|
|
108
|
+
log("gosto: abrindo a tela do gosto no navegador padrão")
|
|
109
|
+
try:
|
|
110
|
+
(abrir or (lambda u: subprocess.Popen(["open", u], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)))(url)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
log(f"gosto: não abriu: {e}")
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
89
116
|
PEDE_NAVEGADOR = re.compile(r"\babr(?:e|a|ir)\b|\bmostr(?:a|e|ar)\b|navegador|visualiz|preview|pr[eé]-?visualiza", re.I)
|
|
90
117
|
|
|
91
118
|
|
|
@@ -369,7 +396,19 @@ class App:
|
|
|
369
396
|
self.cerebro = subprocess.Popen(args, cwd=str(self.pasta), env=env, text=True,
|
|
370
397
|
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
371
398
|
start_new_session=True) # grupo próprio: nada do que o agente faça com processos atinge o app
|
|
372
|
-
|
|
399
|
+
linhas_err: "list[str]" = []
|
|
400
|
+
cer = self.cerebro
|
|
401
|
+
|
|
402
|
+
def ler_err(): # ao vivo: a linha do gosto tem que abrir a tela ENQUANTO o motor espera
|
|
403
|
+
for l in cer.stderr:
|
|
404
|
+
if not abrir_gosto(l):
|
|
405
|
+
linhas_err.append(l)
|
|
406
|
+
th_err = threading.Thread(target=ler_err, daemon=True)
|
|
407
|
+
th_err.start()
|
|
408
|
+
out = cer.stdout.read()
|
|
409
|
+
cer.wait(timeout=900)
|
|
410
|
+
th_err.join(timeout=2)
|
|
411
|
+
err = "".join(linhas_err)
|
|
373
412
|
log(f"cérebro: saiu com código {self.cerebro.returncode}")
|
|
374
413
|
timer.cancel()
|
|
375
414
|
except Exception as e:
|
|
@@ -628,8 +667,8 @@ class Chat:
|
|
|
628
667
|
self.js({"conversas": r.get("conversas") or []} if r.get("ok") else {"conversas_erro": r.get("erro") or "Não deu pra carregar as conversas."})
|
|
629
668
|
|
|
630
669
|
def skills(self) -> None:
|
|
631
|
-
r = self.node("const S=require('./lib/skills.js');
|
|
632
|
-
"console.log(JSON.stringify({ok:true,skills:S.listar().map(s=>({nome:s.nome,descricao:s.descricao||'',ligada
|
|
670
|
+
r = self.node("const S=require('./lib/skills.js');" # listar() já diz se cada skill está ligada
|
|
671
|
+
"console.log(JSON.stringify({ok:true,skills:S.listar().map(s=>({nome:s.nome,descricao:s.descricao||'',ligada:s.ligada!==false}))}))")
|
|
633
672
|
self.js({"skills": r.get("skills") or []} if r.get("ok") else {"erro": r.get("erro")})
|
|
634
673
|
|
|
635
674
|
# ── a ponte ──
|
|
@@ -824,7 +863,7 @@ class Chat:
|
|
|
824
863
|
for l in p.stderr:
|
|
825
864
|
if l.startswith("ferramenta:"):
|
|
826
865
|
self.js({"passo": passo_legivel(l)})
|
|
827
|
-
|
|
866
|
+
elif not abrir_gosto(l):
|
|
828
867
|
resto.append(l)
|
|
829
868
|
th = threading.Thread(target=passos, daemon=True)
|
|
830
869
|
th.start()
|