primocode 10.15.2 → 10.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +74 -9
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +38 -8
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +7 -1
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto/tela.html +1349 -0
- package/lib/estudio/gosto.js +305 -0
- package/lib/estudio/index.js +12 -1
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +70 -12
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +64 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/headless.js +15 -3
- package/lib/local/cerebro.json +285 -7
- package/lib/local/motor.js +5 -2
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +33 -2
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +23 -4
- package/lib/ui.js +2 -2
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/__main__.py +55 -40
- package/voz/primo_voz/app.py +500 -4
- package/voz/primo_voz/chat.html +1773 -0
- package/voz/primo_voz/conversa.py +0 -26
- package/voz/primo_voz/hud.html +8 -2
- package/voz/primo_voz/hud.py +129 -5
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/painel_dados.py +134 -0
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
|
|
|
@@ -147,6 +174,8 @@ class App:
|
|
|
147
174
|
self.vivo = True
|
|
148
175
|
self.trava = threading.Lock()
|
|
149
176
|
self.cache_clima: dict = {}
|
|
177
|
+
self.ao_ouvir = lambda: None
|
|
178
|
+
self.desligando = False # a voz foi fechada de propósito: o motor sair não derruba o app
|
|
150
179
|
|
|
151
180
|
# ── entrada do painel (iterável de linhas JSON) ──
|
|
152
181
|
def __iter__(self):
|
|
@@ -182,6 +211,13 @@ class App:
|
|
|
182
211
|
self.painel({"login": logado()})
|
|
183
212
|
threading.Thread(target=trabalho, daemon=True).start()
|
|
184
213
|
|
|
214
|
+
def entrar_e_esperar(self, limite: float = 600) -> None:
|
|
215
|
+
"""O chat espera o login terminar para mostrar a conta e as conversas."""
|
|
216
|
+
self.entrar()
|
|
217
|
+
fim = time.time() + limite
|
|
218
|
+
while time.time() < fim and not logado():
|
|
219
|
+
time.sleep(1)
|
|
220
|
+
|
|
185
221
|
def ambiente(self) -> dict:
|
|
186
222
|
env = dict(os.environ)
|
|
187
223
|
extra = [str(Path(self.node).parent)] if self.node else []
|
|
@@ -230,10 +266,14 @@ class App:
|
|
|
230
266
|
elif t == "parar":
|
|
231
267
|
self.matar_cerebro()
|
|
232
268
|
elif t == "ouvi" and str(m.get("texto") or "").strip():
|
|
269
|
+
self.ao_ouvir() # pedido falado: a tela de voz vem pra frente
|
|
233
270
|
threading.Thread(target=self.pensar, args=(str(m["texto"]).strip(),), daemon=True).start()
|
|
234
271
|
elif t == "fim":
|
|
235
272
|
break
|
|
236
273
|
log("motor de voz: saiu")
|
|
274
|
+
if self.desligando:
|
|
275
|
+
self.desligando = False
|
|
276
|
+
return
|
|
237
277
|
if self.vivo:
|
|
238
278
|
self.vivo = False
|
|
239
279
|
os._exit(0)
|
|
@@ -356,7 +396,19 @@ class App:
|
|
|
356
396
|
self.cerebro = subprocess.Popen(args, cwd=str(self.pasta), env=env, text=True,
|
|
357
397
|
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
358
398
|
start_new_session=True) # grupo próprio: nada do que o agente faça com processos atinge o app
|
|
359
|
-
|
|
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)
|
|
360
412
|
log(f"cérebro: saiu com código {self.cerebro.returncode}")
|
|
361
413
|
timer.cancel()
|
|
362
414
|
except Exception as e:
|
|
@@ -423,12 +475,456 @@ class App:
|
|
|
423
475
|
self.motor.kill()
|
|
424
476
|
log("app: encerrado")
|
|
425
477
|
|
|
426
|
-
|
|
478
|
+
# ── a voz só liga pelo botão: com o chat na frente, o microfone fica fechado ──
|
|
479
|
+
def ligar_voz(self) -> None:
|
|
480
|
+
with self.trava:
|
|
481
|
+
if self.motor and self.motor.poll() is None:
|
|
482
|
+
return
|
|
483
|
+
self.desligando = False
|
|
484
|
+
self.subir_motor()
|
|
485
|
+
|
|
486
|
+
def desligar_voz(self) -> None:
|
|
487
|
+
with self.trava:
|
|
488
|
+
if not self.motor or self.motor.poll() is not None:
|
|
489
|
+
return
|
|
490
|
+
self.desligando = True
|
|
491
|
+
m, self.motor = self.motor, None
|
|
492
|
+
log("motor de voz: desligando (voltou pro chat)")
|
|
493
|
+
try:
|
|
494
|
+
m.stdin.write(json.dumps({"tipo": "fim"}) + "\n")
|
|
495
|
+
m.stdin.flush()
|
|
496
|
+
m.wait(timeout=2)
|
|
497
|
+
except Exception:
|
|
498
|
+
m.kill()
|
|
499
|
+
|
|
500
|
+
def iniciar(self, js, p, com_chat: bool = True) -> None:
|
|
427
501
|
self.js = js
|
|
428
|
-
|
|
502
|
+
if not com_chat: # só o modo voz puro (sem chat) abre ouvindo
|
|
503
|
+
threading.Timer(0.5, self.subir_motor).start()
|
|
429
504
|
threading.Timer(2.0, lambda: self.painel({"pasta": self.rotulo(), "login": logado()})).start()
|
|
430
505
|
|
|
431
506
|
|
|
507
|
+
# ── o que aparece embaixo de uma mensagem: os arquivos citados, mostrados ali mesmo ──
|
|
508
|
+
TIPOS = {"video": {".mp4", ".mov", ".webm", ".m4v"}, "imagem": {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif"},
|
|
509
|
+
"audio": {".mp3", ".wav", ".m4a", ".aac", ".ogg", ".flac"}, "pdf": {".pdf"}, "html": {".html", ".htm"},
|
|
510
|
+
"codigo": {".js", ".jsx", ".ts", ".tsx", ".py", ".css", ".json", ".md", ".txt", ".sh", ".yml", ".yaml", ".toml",
|
|
511
|
+
".rs", ".go", ".java", ".kt", ".swift", ".rb", ".php", ".sql", ".csv", ".xml", ".env.example"}}
|
|
512
|
+
CAMINHO_ABS = re.compile(r"(?<![\w/])(?:~|/(?:Users|Volumes|tmp|private|opt|var|Applications))(?:/[\w.@+\-()À-ÿ]+)+/?")
|
|
513
|
+
CAMINHO_REL = re.compile(r"(?<![\w/~.])(?:[\w.@+\-À-ÿ]+/)+[\w.@+\-À-ÿ]+\.\w{1,5}\b")
|
|
514
|
+
CRASE = re.compile(r"`([^`\n]{2,300})`")
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def tipo_de(c: Path) -> str:
|
|
518
|
+
if c.is_dir():
|
|
519
|
+
return "pasta"
|
|
520
|
+
ext = c.suffix.lower()
|
|
521
|
+
return next((t for t, exts in TIPOS.items() if ext in exts), "arquivo")
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def midias_do_texto(texto: str, pasta: Path, limite: int = 8) -> "list[dict]":
|
|
525
|
+
"""Os arquivos e pastas que a mensagem cita e que EXISTEM: caminho absoluto, ~/..., ou relativo à pasta do
|
|
526
|
+
projeto (entre crases ou com barra). `achado` é o texto como apareceu, para a tela virar ele num botão."""
|
|
527
|
+
vistos, fora = set(), []
|
|
528
|
+
candidatos = [m.group(1).strip() for m in CRASE.finditer(texto or "")]
|
|
529
|
+
candidatos += [m.group(0) for m in CAMINHO_ABS.finditer(texto or "")] + [m.group(0) for m in CAMINHO_REL.finditer(texto or "")]
|
|
530
|
+
for achado in candidatos:
|
|
531
|
+
bruto = achado.rstrip(".,;:)!?").strip()
|
|
532
|
+
if not bruto or "\n" in bruto or bruto.startswith(("http://", "https://")):
|
|
533
|
+
continue
|
|
534
|
+
c = Path(os.path.expanduser(bruto))
|
|
535
|
+
if not c.is_absolute():
|
|
536
|
+
if "/" not in bruto and "." not in bruto:
|
|
537
|
+
continue
|
|
538
|
+
c = pasta / c
|
|
539
|
+
try:
|
|
540
|
+
c = c.resolve()
|
|
541
|
+
existe = c.exists()
|
|
542
|
+
except (OSError, RuntimeError):
|
|
543
|
+
continue
|
|
544
|
+
if not existe or str(c) in vistos or c == HOME or c == Path("/"):
|
|
545
|
+
continue
|
|
546
|
+
vistos.add(str(c))
|
|
547
|
+
tipo = tipo_de(c)
|
|
548
|
+
item = {"achado": bruto, "caminho": str(c), "nome": c.name, "tipo": tipo, "url": c.as_uri(),
|
|
549
|
+
"tamanho": c.stat().st_size if c.is_file() else 0}
|
|
550
|
+
if tipo == "codigo" and item["tamanho"] <= 200_000:
|
|
551
|
+
try:
|
|
552
|
+
item["trecho"] = "\n".join(c.read_text("utf-8", errors="replace").splitlines()[:40])[:4000]
|
|
553
|
+
except OSError:
|
|
554
|
+
pass
|
|
555
|
+
fora.append(item)
|
|
556
|
+
if len(fora) >= limite:
|
|
557
|
+
break
|
|
558
|
+
return fora
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# Os comandos de / no app. Os do terminal que viram ação da própria janela, e os que não se aplicam a ela.
|
|
562
|
+
COMANDOS_DO_APP = {"/clear": "nova", "/chats": "buscar", "/login": "entrar", "/logout": "sair_conta", "/account": "conta",
|
|
563
|
+
"/mode": "modo", "/api": "modelos", "/config": "ajustes", "/skills": "skills", "/marketplace": "skills",
|
|
564
|
+
"/attach": "anexar", "/detach": "desanexar", "/copiar": "copiar", "/voice": "voz", "/voice nome": "voz_nome",
|
|
565
|
+
"/dir": "pasta", "/help": "ajuda", "/effort": "effort", "/permissoes": "permissoes"}
|
|
566
|
+
# "perguntar" precisa de alguém no terminal para responder: no app (sem terminal) não se aplica.
|
|
567
|
+
PERMISSOES_APP = [("auto", "roda tudo, mas pergunta antes do que não tem volta"), ("livre", "faz tudo sem perguntar"),
|
|
568
|
+
("plano", "só lê e planeja, não muda nada")]
|
|
569
|
+
COMANDOS_FORA = {"/cls", "/exit", "/uninstall", "/atalhos", "/mascot", "/offline", "/files", "/voice painel", "/voice parar", "!comando"}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def comandos_para_o_app(lista: list) -> list:
|
|
573
|
+
"""A lista do terminal (`menu-comandos`) com o tipo de cada um: app (a janela faz), terminal (roda e mostra a
|
|
574
|
+
saída) ou pedido (comando da pessoa: vira um pedido ao agente)."""
|
|
575
|
+
fora = []
|
|
576
|
+
for c in lista:
|
|
577
|
+
nome = str(c.get("nome") or "")
|
|
578
|
+
if not nome or nome in COMANDOS_FORA:
|
|
579
|
+
continue
|
|
580
|
+
item = {"nome": nome, "descricao": str(c.get("descricao") or "")}
|
|
581
|
+
if nome in COMANDOS_DO_APP:
|
|
582
|
+
item.update(tipo="app", acao=COMANDOS_DO_APP[nome])
|
|
583
|
+
else:
|
|
584
|
+
item["tipo"] = "terminal" if c.get("embutido") else "pedido"
|
|
585
|
+
fora.append(item)
|
|
586
|
+
return fora
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class Chat:
|
|
590
|
+
"""A tela do chat (chat.html): manda e recebe pela ponte `chat` do hud.py. O cérebro é o mesmo da voz
|
|
591
|
+
(`primocode -p ... --output-format json`), com `--resume <id>` para seguir a conversa da nuvem."""
|
|
592
|
+
|
|
593
|
+
MAX_ANEXO = 25 * 1024 * 1024
|
|
594
|
+
|
|
595
|
+
def __init__(self, app: App) -> None:
|
|
596
|
+
self.app = app
|
|
597
|
+
self.js = lambda m: None
|
|
598
|
+
self.mostrar_voz = lambda: None
|
|
599
|
+
self.parado = False
|
|
600
|
+
app.ao_ouvir = lambda: self.mostrar_voz()
|
|
601
|
+
|
|
602
|
+
def raiz(self) -> "Path | None":
|
|
603
|
+
r = Path(self.app.primocode).resolve().parent.parent if self.app.primocode else None
|
|
604
|
+
return r if r and (r / "lib").is_dir() else None
|
|
605
|
+
|
|
606
|
+
def node(self, codigo: str, timeout: int = 60) -> dict:
|
|
607
|
+
"""Roda um pedaço de JS na raiz do PrimoCode e lê a última linha de JSON que ele imprimir."""
|
|
608
|
+
r = self.raiz()
|
|
609
|
+
if not (self.app.node and r):
|
|
610
|
+
return {"ok": False, "erro": "Não achei o PrimoCode instalado. Rode primocode voz instalar-app no terminal."}
|
|
611
|
+
try:
|
|
612
|
+
p = subprocess.run([self.app.node, "-e", codigo], cwd=str(r), env=self.app.ambiente(), capture_output=True, text=True, timeout=timeout)
|
|
613
|
+
except Exception as e:
|
|
614
|
+
return {"ok": False, "erro": str(e)}
|
|
615
|
+
for l in reversed(p.stdout.strip().splitlines()):
|
|
616
|
+
try:
|
|
617
|
+
return json.loads(l)
|
|
618
|
+
except ValueError:
|
|
619
|
+
continue
|
|
620
|
+
log("chat node: " + (p.stderr or "")[-300:])
|
|
621
|
+
return {"ok": False, "erro": "O PrimoCode não respondeu."}
|
|
622
|
+
|
|
623
|
+
def cli(self, *args: str, timeout: int = 60) -> dict:
|
|
624
|
+
if not (self.app.node and self.app.primocode):
|
|
625
|
+
return {"ok": False, "erro": "Não achei o PrimoCode instalado."}
|
|
626
|
+
try:
|
|
627
|
+
p = subprocess.run([self.app.node, self.app.primocode, *args], cwd=str(self.app.pasta), env=self.app.ambiente(),
|
|
628
|
+
capture_output=True, text=True, timeout=timeout)
|
|
629
|
+
except Exception as e:
|
|
630
|
+
return {"ok": False, "erro": str(e)}
|
|
631
|
+
for l in reversed(p.stdout.strip().splitlines()):
|
|
632
|
+
try:
|
|
633
|
+
return json.loads(l)
|
|
634
|
+
except ValueError:
|
|
635
|
+
continue
|
|
636
|
+
return {"ok": False, "erro": "O PrimoCode não respondeu."}
|
|
637
|
+
|
|
638
|
+
# ── o que a tela mostra ──
|
|
639
|
+
def conta(self) -> dict:
|
|
640
|
+
c = _json(PASTA_PC / "conta.json")
|
|
641
|
+
ate = c.get("planoAte")
|
|
642
|
+
if isinstance(ate, (int, float)):
|
|
643
|
+
ate = time.strftime("%d/%m/%Y", time.localtime(ate / 1000 if ate > 1e11 else ate))
|
|
644
|
+
return {"logado": logado(), "email": c.get("email") or "", "plano": c.get("plano") or "", "planoAte": ate or ""}
|
|
645
|
+
|
|
646
|
+
def modo(self) -> str:
|
|
647
|
+
return "chat" if _json(PASTA_PC / "config.json").get("mode") == "chat" else "agent"
|
|
648
|
+
|
|
649
|
+
def ajustes(self) -> dict:
|
|
650
|
+
r = self.node("const P=require('./lib/local/provedor.js');const a=P.atual();"
|
|
651
|
+
"console.log(JSON.stringify({ok:true,atual:a?{id:a.id,nome:a.nome,cli:a.cli,chave:a.chave?P.mascarar(a.chave):''}:null,"
|
|
652
|
+
"lista:Object.entries(P.PROVEDORES).filter(([,p])=>!p.cli).map(([id,p])=>({id,nome:p.nome}))}))")
|
|
653
|
+
atual = r.get("atual") if r.get("ok") else None
|
|
654
|
+
voz = _json(PASTA_PC / "voz.json")
|
|
655
|
+
cfg = _json(PASTA_PC / "config.json")
|
|
656
|
+
ef = self.node("const E=require('./lib/effort.js');console.log(JSON.stringify({ok:true,niveis:E.NIVEIS.map(n=>({nome:n.nome,resumo:n.resumo}))}))")
|
|
657
|
+
perm = cfg.get("permissoes") if cfg.get("permissoes") in dict(PERMISSOES_APP) else "auto"
|
|
658
|
+
return {"modo": self.modo(),
|
|
659
|
+
"effort": {"atual": str(cfg.get("effort") or "medium"), "niveis": ef.get("niveis") or []},
|
|
660
|
+
"permissoes": {"atual": perm, "opcoes": [{"nome": n, "resumo": r} for n, r in PERMISSOES_APP]},
|
|
661
|
+
"provedor": {"id": atual["id"], "nome": atual["nome"], "chave_mascarada": atual.get("chave") or ""} if atual else {"id": "primo", "nome": "Primo", "chave_mascarada": ""},
|
|
662
|
+
"provedores_chave": r.get("lista") or [], "pasta": str(self.app.pasta), "voz": {"nome": voz.get("nome") or "Primo", "sons": voz.get("sons", True) is not False},
|
|
663
|
+
"conta": self.conta()}
|
|
664
|
+
|
|
665
|
+
def conversas(self) -> None:
|
|
666
|
+
r = self.cli("conversas", "--json", "--limite", "40")
|
|
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."})
|
|
668
|
+
|
|
669
|
+
def skills(self) -> None:
|
|
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}))}))")
|
|
672
|
+
self.js({"skills": r.get("skills") or []} if r.get("ok") else {"erro": r.get("erro")})
|
|
673
|
+
|
|
674
|
+
# ── a ponte ──
|
|
675
|
+
def receber(self, m: dict) -> None:
|
|
676
|
+
a = m.get("acao")
|
|
677
|
+
trab = None
|
|
678
|
+
if a == "pronto":
|
|
679
|
+
def trab():
|
|
680
|
+
self.js({"inicio": {**self.conta(), "pasta": str(self.app.pasta), "modo": self.modo()}})
|
|
681
|
+
r = self.cli("menu-comandos")
|
|
682
|
+
if r.get("ok"):
|
|
683
|
+
self.js({"comandos": comandos_para_o_app(r.get("comandos") or [])})
|
|
684
|
+
if logado():
|
|
685
|
+
self.conversas()
|
|
686
|
+
elif a == "enviar":
|
|
687
|
+
self.parado = False
|
|
688
|
+
trab = lambda: self.pensar(str(m.get("texto") or ""), m.get("conversa"), m.get("modo") or self.modo(), m.get("anexos") or [])
|
|
689
|
+
elif a == "comando" and str(m.get("texto") or "").startswith("/"):
|
|
690
|
+
self.parado = False
|
|
691
|
+
trab = lambda: self.pensar(str(m["texto"]), m.get("conversa"), self.modo(), [], comando=True)
|
|
692
|
+
elif a == "parar":
|
|
693
|
+
self.parado = True
|
|
694
|
+
self.app.matar_cerebro()
|
|
695
|
+
elif a == "conversas":
|
|
696
|
+
trab = self.conversas
|
|
697
|
+
elif a == "abrir" and m.get("id"):
|
|
698
|
+
def trab():
|
|
699
|
+
r = self.cli("conversa", str(m["id"]), "--json")
|
|
700
|
+
if not r.get("ok"):
|
|
701
|
+
self.js({"erro": r.get("erro") or "Não abriu a conversa."})
|
|
702
|
+
return
|
|
703
|
+
msgs = [{**x, "midias": midias_do_texto(x.get("texto", ""), self.app.pasta)} for x in (r.get("mensagens") or [])]
|
|
704
|
+
self.js({"conversa": {"id": r.get("id"), "titulo": r.get("titulo"), "mensagens": msgs}})
|
|
705
|
+
elif a == "entrar":
|
|
706
|
+
def trab():
|
|
707
|
+
self.app.entrar_e_esperar()
|
|
708
|
+
self.js({"login": self.conta()})
|
|
709
|
+
if logado():
|
|
710
|
+
self.conversas()
|
|
711
|
+
elif a == "sair_conta":
|
|
712
|
+
def trab():
|
|
713
|
+
self.node("require('./lib/conta.js').sair();console.log('{\"ok\":true}')")
|
|
714
|
+
self.js({"login": self.conta()})
|
|
715
|
+
self.js({"conversas": []})
|
|
716
|
+
elif a == "excluir" and m.get("id"):
|
|
717
|
+
def trab():
|
|
718
|
+
r = self.cli("conversa", str(m["id"]), "--apagar", "--json", timeout=120)
|
|
719
|
+
if not r.get("ok"):
|
|
720
|
+
self.js({"erro": "Não apaguei a conversa: " + str(r.get("erro") or "sem resposta")})
|
|
721
|
+
self.conversas()
|
|
722
|
+
elif a == "excluir_todas":
|
|
723
|
+
def trab():
|
|
724
|
+
r = self.cli("conversas", "--apagar-todas", "--json", timeout=600)
|
|
725
|
+
if not r.get("ok"):
|
|
726
|
+
self.js({"erro": "Não apaguei todas: " + str(r.get("erro") or "sem resposta")})
|
|
727
|
+
self.conversas()
|
|
728
|
+
elif a == "skills":
|
|
729
|
+
trab = self.skills
|
|
730
|
+
elif a == "skill" and m.get("nome"):
|
|
731
|
+
def trab():
|
|
732
|
+
self.node(f"require('./lib/skills.js').ligar({json.dumps(str(m['nome']))},{'true' if m.get('ligada') else 'false'});console.log('{{\"ok\":true}}')")
|
|
733
|
+
self.skills()
|
|
734
|
+
elif a == "ajustes":
|
|
735
|
+
trab = lambda: self.js({"ajustes": self.ajustes()})
|
|
736
|
+
elif a == "ajuste":
|
|
737
|
+
trab = lambda: self.ajuste(str(m.get("chave") or ""), m.get("valor"))
|
|
738
|
+
elif a == "soltar":
|
|
739
|
+
trab = lambda: self.soltar(str(m.get("nome") or "arquivo"), str(m.get("base64") or ""))
|
|
740
|
+
if trab:
|
|
741
|
+
threading.Thread(target=trab, daemon=True).start()
|
|
742
|
+
|
|
743
|
+
def ajuste(self, chave: str, valor) -> None:
|
|
744
|
+
if chave in ("modo", "effort", "permissoes"):
|
|
745
|
+
cfg = _json(PASTA_PC / "config.json")
|
|
746
|
+
if chave == "modo":
|
|
747
|
+
cfg["mode"] = "chat" if valor == "chat" else "primocode"
|
|
748
|
+
elif chave == "effort" and str(valor) in ("low", "medium", "high", "xhigh", "max", "ultracode"):
|
|
749
|
+
cfg["effort"] = str(valor)
|
|
750
|
+
elif chave == "permissoes" and str(valor) in dict(PERMISSOES_APP):
|
|
751
|
+
cfg["permissoes"] = str(valor)
|
|
752
|
+
else:
|
|
753
|
+
self.js({"erro": f"Valor inválido para {chave}: {valor}"})
|
|
754
|
+
(PASTA_PC / "config.json").write_text(json.dumps(cfg, ensure_ascii=False, indent=2), "utf-8")
|
|
755
|
+
elif chave == "provedor" and isinstance(valor, dict):
|
|
756
|
+
pid = str(valor.get("provedor") or "")
|
|
757
|
+
if pid == "primo":
|
|
758
|
+
r = self.node("require('./lib/local/provedor.js').desligar();console.log('{\"ok\":true}')")
|
|
759
|
+
else:
|
|
760
|
+
r = self.node("const P=require('./lib/local/provedor.js');const p=P.PROVEDORES[" + json.dumps(pid) + "];"
|
|
761
|
+
"if(!p){console.log(JSON.stringify({ok:false,erro:'Provedor desconhecido.'}));}else{"
|
|
762
|
+
"P.salvar({provedor:" + json.dumps(pid) + ",chave:" + json.dumps(str(valor.get("chave") or "")) + ",modelo:p.modelos[0]});"
|
|
763
|
+
"console.log(JSON.stringify({ok:true}))}")
|
|
764
|
+
if not r.get("ok"):
|
|
765
|
+
self.js({"erro": r.get("erro") or "Não deu pra trocar o modelo."})
|
|
766
|
+
elif chave == "voz_nome":
|
|
767
|
+
from primo_voz import identidade
|
|
768
|
+
nome = identidade._nome_valido(str(valor or ""))
|
|
769
|
+
if not nome:
|
|
770
|
+
self.js({"erro": "O nome precisa ter 1 ou 2 palavras, até 20 letras."})
|
|
771
|
+
else:
|
|
772
|
+
self.gravar_voz(nome=nome)
|
|
773
|
+
self.app.js({"nome": nome})
|
|
774
|
+
elif chave == "voz_sons":
|
|
775
|
+
self.gravar_voz(sons=bool(valor))
|
|
776
|
+
self.js({"ajustes": self.ajustes()})
|
|
777
|
+
|
|
778
|
+
def gravar_voz(self, **campos) -> None:
|
|
779
|
+
arq = PASTA_PC / "voz.json"
|
|
780
|
+
d = _json(arq)
|
|
781
|
+
d.update(campos)
|
|
782
|
+
arq.write_text(json.dumps(d, ensure_ascii=False, indent=2), "utf-8")
|
|
783
|
+
|
|
784
|
+
def escolher_pasta(self, caminho: str) -> None:
|
|
785
|
+
d = Path(caminho)
|
|
786
|
+
if d.is_dir():
|
|
787
|
+
self.app.pasta, self.app.pasta_modo = d, "/dir"
|
|
788
|
+
gravar_pasta(d)
|
|
789
|
+
self.app.painel({"pasta": self.app.rotulo()})
|
|
790
|
+
self.js({"pasta": str(d)})
|
|
791
|
+
|
|
792
|
+
# ── anexos: tudo vai para <pasta>/anexos e o caminho entra no pedido ──
|
|
793
|
+
def destino(self, nome: str) -> Path:
|
|
794
|
+
pasta = self.app.pasta / "anexos"
|
|
795
|
+
pasta.mkdir(parents=True, exist_ok=True)
|
|
796
|
+
base = re.sub(r"[^\w.\- ]", "_", Path(nome).name).strip() or "arquivo"
|
|
797
|
+
alvo, k = pasta / base, 1
|
|
798
|
+
while alvo.exists():
|
|
799
|
+
alvo = pasta / f"{Path(base).stem}-{k}{Path(base).suffix}"
|
|
800
|
+
k += 1
|
|
801
|
+
return alvo
|
|
802
|
+
|
|
803
|
+
def anexar_caminhos(self, caminhos: "list[str]") -> None:
|
|
804
|
+
import shutil
|
|
805
|
+
for c in caminhos:
|
|
806
|
+
o = Path(c)
|
|
807
|
+
if not o.is_file():
|
|
808
|
+
continue
|
|
809
|
+
if o.stat().st_size > self.MAX_ANEXO:
|
|
810
|
+
self.js({"erro": f"{o.name} passa de 25 MB."})
|
|
811
|
+
continue
|
|
812
|
+
alvo = self.destino(o.name)
|
|
813
|
+
shutil.copy2(o, alvo)
|
|
814
|
+
self.js({"anexo": {"nome": o.name, "caminho": str(alvo)}})
|
|
815
|
+
|
|
816
|
+
def soltar(self, nome: str, b64: str) -> None:
|
|
817
|
+
import base64
|
|
818
|
+
try:
|
|
819
|
+
dados = base64.b64decode(b64, validate=False)
|
|
820
|
+
except Exception:
|
|
821
|
+
self.js({"erro": f"Não li {nome}."})
|
|
822
|
+
return
|
|
823
|
+
if len(dados) > self.MAX_ANEXO:
|
|
824
|
+
self.js({"erro": f"{nome} passa de 25 MB."})
|
|
825
|
+
return
|
|
826
|
+
alvo = self.destino(nome)
|
|
827
|
+
alvo.write_bytes(dados)
|
|
828
|
+
self.js({"anexo": {"nome": nome, "caminho": str(alvo)}})
|
|
829
|
+
|
|
830
|
+
# ── o cérebro ──
|
|
831
|
+
def pensar(self, texto: str, conversa, modo: str, anexos: list, comando: bool = False) -> None:
|
|
832
|
+
"""comando=True: `texto` é um /comando do terminal; a saída volta como {saida} (texto de terminal), não como resposta."""
|
|
833
|
+
texto = texto.strip()
|
|
834
|
+
if anexos:
|
|
835
|
+
texto = (texto or "Olha esses arquivos.") + "\n\nAnexos:\n" + "\n".join(f"- {a}" for a in anexos)
|
|
836
|
+
if not texto:
|
|
837
|
+
return
|
|
838
|
+
if not comando and not logado(): # /help, /doctor, /version… funcionam sem conta
|
|
839
|
+
self.js({"login": self.conta()})
|
|
840
|
+
self.js({"erro": "Entre na sua conta do PrimoCode pra conversar."})
|
|
841
|
+
return
|
|
842
|
+
if not (self.app.node and self.app.primocode and Path(self.app.primocode).exists()):
|
|
843
|
+
self.js({"erro": "Não achei o PrimoCode instalado. Rode primocode voz instalar-app no terminal."})
|
|
844
|
+
return
|
|
845
|
+
with self.app.trava:
|
|
846
|
+
env = self.app.ambiente()
|
|
847
|
+
env.update({"PRIMOCODE_APP": "1", "PRIMOCODE_RAIZ": str(RAIZ), "PRIMOCODE_LOG_FERRAMENTAS": "1",
|
|
848
|
+
"PRIMOCODE_MODO": "chat" if modo == "chat" else "primocode"})
|
|
849
|
+
if comando and re.match(r"/(open|abrir)\b", texto):
|
|
850
|
+
env["PRIMOCODE_SEM_NAVEGADOR"] = "0" # /open e /abrir existem para abrir o navegador
|
|
851
|
+
if self.app.pasta != RAIZ:
|
|
852
|
+
env["PRIMOCODE_PASTA"], env["PRIMOCODE_PASTA_ORIGEM"] = str(self.app.pasta), ("/dir" if self.app.pasta_modo == "/dir" else "projeto")
|
|
853
|
+
args = [self.app.node, self.app.primocode, "-p", texto, "--output-format", "json"] + (["--resume", str(conversa)] if conversa else [])
|
|
854
|
+
log(f"chat: modo={modo} cwd={self.app.pasta} conversa={conversa or 'nova'}")
|
|
855
|
+
t0 = time.time()
|
|
856
|
+
try:
|
|
857
|
+
p = subprocess.Popen(args, cwd=str(self.app.pasta), env=env, text=True, stdin=subprocess.DEVNULL,
|
|
858
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True)
|
|
859
|
+
self.app.cerebro = p
|
|
860
|
+
resto: "list[str]" = []
|
|
861
|
+
|
|
862
|
+
def passos(): # cada ferramenta que o agente usa vira uma linha ao vivo no chat
|
|
863
|
+
for l in p.stderr:
|
|
864
|
+
if l.startswith("ferramenta:"):
|
|
865
|
+
self.js({"passo": passo_legivel(l)})
|
|
866
|
+
elif not abrir_gosto(l):
|
|
867
|
+
resto.append(l)
|
|
868
|
+
th = threading.Thread(target=passos, daemon=True)
|
|
869
|
+
th.start()
|
|
870
|
+
out = p.stdout.read()
|
|
871
|
+
p.wait(timeout=900)
|
|
872
|
+
th.join(timeout=2)
|
|
873
|
+
except Exception as e:
|
|
874
|
+
log(f"chat: cérebro falhou: {e}")
|
|
875
|
+
self.js({"erro": "O PrimoCode não respondeu."})
|
|
876
|
+
return
|
|
877
|
+
if self.parado:
|
|
878
|
+
self.js({"erro": "Parei."})
|
|
879
|
+
return
|
|
880
|
+
res = {}
|
|
881
|
+
for l in reversed((out or "").strip().splitlines()):
|
|
882
|
+
try:
|
|
883
|
+
res = json.loads(l)
|
|
884
|
+
break
|
|
885
|
+
except ValueError:
|
|
886
|
+
continue
|
|
887
|
+
if comando:
|
|
888
|
+
saida = str(res.get("result") or "").strip()
|
|
889
|
+
if res.get("is_error"):
|
|
890
|
+
self.js({"erro": saida or "O comando falhou."})
|
|
891
|
+
else:
|
|
892
|
+
self.js({"saida": {"comando": texto.split()[0], "texto": saida or "(sem saída)", "conversa": conversa, "midias": midias_do_texto(saida, self.app.pasta)}})
|
|
893
|
+
return
|
|
894
|
+
# o markdown cru da resposta (o -p json manda); sem ele, o texto do terminal sem os enfeites
|
|
895
|
+
txt = str(res.get("markdown") or "").strip() or \
|
|
896
|
+
"\n".join(l for l in str(res.get("result") or "").splitlines() if not l.lstrip().startswith("⎿")).replace("●", "").strip()
|
|
897
|
+
log(f"chat: {time.time() - t0:.1f}s, {len(txt)} caracteres, erro={bool(res.get('is_error'))}")
|
|
898
|
+
if res.get("is_error") or not txt:
|
|
899
|
+
self.js({"erro": txt or ("".join(resto)[-300:].strip() or "Não consegui uma resposta agora.")})
|
|
900
|
+
return
|
|
901
|
+
self.js({"resposta": {"texto": txt, "conversa": res.get("conversa") or conversa, "midias": midias_do_texto(txt, self.app.pasta)}})
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
FERRAMENTA_LEGIVEL = {"read_file": "Lendo", "write_file": "Escrevendo", "edit_file": "Editando", "multi_edit": "Editando",
|
|
905
|
+
"run_shell": "Rodando", "run_in_new_terminal": "Abrindo um terminal:", "list_dir": "Olhando a pasta",
|
|
906
|
+
"grep": "Procurando", "glob": "Procurando arquivos", "open_in_browser": "Abrindo no navegador",
|
|
907
|
+
"pesquisar_referencia": "Pesquisando referências", "write_todos": "Organizando as etapas",
|
|
908
|
+
"estudio_video": "Montando o vídeo", "estudio_motion": "Animando", "estudio_render": "Renderizando",
|
|
909
|
+
"estudio_editar": "Editando o vídeo", "estudio_narrar": "Gravando a narração", "estudio_site": "Fazendo o site",
|
|
910
|
+
"estudio_ae": "Animando no estilo After Effects", "estudio_midia": "Buscando mídia", "estudio_plano": "Planejando",
|
|
911
|
+
"estudio_projeto": "Abrindo o projeto"}
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def passo_legivel(linha: str) -> str:
|
|
915
|
+
"""`ferramenta: read_file {"path":"src/a.js"}` → `Lendo src/a.js`. O que não reconhece, mostra o nome."""
|
|
916
|
+
m = re.match(r"ferramenta:\s*(\S+)\s*(.*)", linha.strip())
|
|
917
|
+
if not m:
|
|
918
|
+
return linha.strip()[:120]
|
|
919
|
+
nome, resto = m.group(1), m.group(2)
|
|
920
|
+
try:
|
|
921
|
+
a = json.loads(resto) if resto else {}
|
|
922
|
+
except ValueError:
|
|
923
|
+
a = {}
|
|
924
|
+
alvo = next((str(a[k]) for k in ("path", "file_path", "file", "command", "cmd", "pattern", "query", "url", "dir") if isinstance(a, dict) and a.get(k)), "")
|
|
925
|
+
return (FERRAMENTA_LEGIVEL.get(nome, nome) + (" " + alvo if alvo else "")).strip()[:120]
|
|
926
|
+
|
|
927
|
+
|
|
432
928
|
def main() -> int:
|
|
433
929
|
import fcntl
|
|
434
930
|
import atexit
|
|
@@ -444,7 +940,7 @@ def main() -> int:
|
|
|
444
940
|
app = App()
|
|
445
941
|
atexit.register(app.sair)
|
|
446
942
|
from primo_voz import hud
|
|
447
|
-
return hud.main(entrada=iter(app), saida=app.saida_do_painel, ao_iniciar=app.iniciar, ao_sair=app.sair)
|
|
943
|
+
return hud.main(entrada=iter(app), saida=app.saida_do_painel, ao_iniciar=app.iniciar, ao_sair=app.sair, chat=Chat(app))
|
|
448
944
|
|
|
449
945
|
|
|
450
946
|
if __name__ == "__main__":
|