primocode 10.0.0 → 10.1.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/README.md +4 -4
- package/bin/primocode.js +88 -3
- package/lib/act.js +122 -20
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +6 -3
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -2
- package/lib/entrada.js +73 -9
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +63 -25
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +41 -10
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +1471 -0
- package/lib/estudio/hibrido.js +169 -0
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +366 -47
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +990 -0
- package/lib/estudio/kit/index.jsx +4 -1
- package/lib/estudio/kit/tres.jsx +375 -0
- package/lib/estudio/kit.js +9 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +27 -0
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +103 -14
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +183 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +51 -1
- package/lib/tools.js +53 -8
- package/lib/ui.js +41 -6
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +1 -1
- package/lib/video/filters.js +39 -6
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +30 -5
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +67 -14
- package/lib/video/texto.js +6 -4
- package/lib/video/timeline.js +9 -1
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +5 -4
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/voz_kokoro.py +98 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.0.
|
|
1
|
+
# PrimoCode v10.0.9
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.0.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.0.9
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -352,7 +352,7 @@ primocode -c # continua a última conversa salva (--co
|
|
|
352
352
|
|
|
353
353
|
```json
|
|
354
354
|
{
|
|
355
|
-
"server": "https://primocode-server.
|
|
355
|
+
"server": "https://primocode-server-production.up.railway.app",
|
|
356
356
|
"model": "main",
|
|
357
357
|
"token": null
|
|
358
358
|
}
|
|
@@ -361,7 +361,7 @@ primocode -c # continua a última conversa salva (--co
|
|
|
361
361
|
Ou por variável de ambiente — elas têm prioridade:
|
|
362
362
|
|
|
363
363
|
```bash
|
|
364
|
-
export PRIMOCODE_SERVER="https://primocode-server.
|
|
364
|
+
export PRIMOCODE_SERVER="https://primocode-server-production.up.railway.app"
|
|
365
365
|
export PRIMOCODE_MODEL="main"
|
|
366
366
|
export PRIMOCODE_TOKEN="..." # só se o servidor exigir
|
|
367
367
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -36,6 +36,7 @@ const skills = require('../lib/skills');
|
|
|
36
36
|
const checkpoint = require('../lib/checkpoint');
|
|
37
37
|
const comandosLib = require('../lib/comandos');
|
|
38
38
|
const marketplace = require('../lib/marketplace');
|
|
39
|
+
const portaoSkill = require('../lib/portao-skill');
|
|
39
40
|
const vozModo = require('../lib/voz');
|
|
40
41
|
const conta = require('../lib/conta');
|
|
41
42
|
const nuvem = require('../lib/nuvem');
|
|
@@ -62,6 +63,17 @@ const tempos = require('../lib/tempos');
|
|
|
62
63
|
process.on('uncaughtException', (e) => {
|
|
63
64
|
fala.problema(e);
|
|
64
65
|
});
|
|
66
|
+
/* O Node sai com 0 quando o laço de eventos esvazia — e uma promessa que
|
|
67
|
+
nunca resolve, sem timer nem socket vivo, faz exatamente isso: o pedido
|
|
68
|
+
"termina" calado no meio (visto no vídeo de escopo fechado, 01/10). Com
|
|
69
|
+
tarefa em andamento, sair assim é bug: diz, e sai com erro. */
|
|
70
|
+
process.on('beforeExit', () => {
|
|
71
|
+
if (global.__primocodeTarefa) {
|
|
72
|
+
process.stderr.write(`\n ⚠ o PrimoCode ia sair no meio da tarefa (${global.__primocodeTarefa}). Diga "continua" para retomar.\n`);
|
|
73
|
+
process.exitCode = 3;
|
|
74
|
+
global.__primocodeTarefa = null;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
65
77
|
process.on('unhandledRejection', (e) => {
|
|
66
78
|
fala.problema(e instanceof Error ? e : new Error(String(e)));
|
|
67
79
|
});
|
|
@@ -639,6 +651,12 @@ function pedeProjetoNovo(texto) {
|
|
|
639
651
|
const RE_COMPUTADOR = /\b(abr[ae]|abrir|clica|clique|clicar|digita|digite|digitar|mouse|cursor|teclado|meu computador|no (meu )?mac|na (minha )?tela|aplicativo|spotify|safari|finder|whatsapp|mensagens|m[úu]sica|configura[çc][õo]es|ajustes)\b/i;
|
|
640
652
|
function querComputador(texto) { return RE_COMPUTADOR.test(String(texto || '')); }
|
|
641
653
|
|
|
654
|
+
/* Pedido de VER o navegador ("abre o youtube", "entra no site x", "testa no
|
|
655
|
+
Chrome"). Sem isto as ferramentas browser_* rodam escondidas: pesquisar e
|
|
656
|
+
conferir não precisam abrir aba na tela da pessoa (02/10). */
|
|
657
|
+
const RE_NAVEGADOR = /\b(navegador|chrome|browser|edge|brave|aba|abas|youtube|gmail|instagram|facebook|linkedin|login|loga|logar|(abr[ae]|abrir|entr[ae]|entrar|acess[ae]|acessar|mostr[ae]|testa|testar)\b[^.?!]{0,25}\b(site|p[áa]gina|link|url|google|navegador))\b/i;
|
|
658
|
+
function querNavegador(texto) { return RE_NAVEGADOR.test(String(texto || '')); }
|
|
659
|
+
|
|
642
660
|
/* Guarda onde se está, para a próxima sessão continuar aqui. */
|
|
643
661
|
function entrarNoProjeto(dir, origem = 'projeto') {
|
|
644
662
|
state.workDir = dir;
|
|
@@ -986,10 +1004,26 @@ async function instalarSkill() {
|
|
|
986
1004
|
console.log('');
|
|
987
1005
|
const caminho = String(await ask(' ' + c.brand('❯ ') + c.white('Caminho do SKILL.md: '))).trim();
|
|
988
1006
|
if (!caminho) return;
|
|
989
|
-
const r =
|
|
1007
|
+
const r = await instalarComPortao(caminho);
|
|
990
1008
|
console.log(r.ok ? c.ok(` Skill instalada: ${r.nome}`) : c.err(` ${r.error}`));
|
|
991
1009
|
}
|
|
992
1010
|
|
|
1011
|
+
/* Instala passando pelo portão (lib/portao-skill.js). Achou algo: mostra cada
|
|
1012
|
+
achado com arquivo:linha e faz a mesma pergunta do que não tem volta
|
|
1013
|
+
(autorizarControle) — Enter sozinho é NÃO. */
|
|
1014
|
+
async function instalarComPortao(caminho) {
|
|
1015
|
+
const r = skills.instalarDe(caminho);
|
|
1016
|
+
if (!r.bloqueada) return r;
|
|
1017
|
+
console.log('');
|
|
1018
|
+
console.log(' ' + c.err(r.error));
|
|
1019
|
+
for (const linha of portaoSkill.descrever(r.achados).slice(0, 20)) console.log(' ' + c.warn(linha));
|
|
1020
|
+
if (r.achados.length > 20) console.log(c.dim(` … e mais ${r.achados.length - 20}`));
|
|
1021
|
+
console.log(' ' + c.dim('Leia cada linha: o portão acha padrão, não intenção.'));
|
|
1022
|
+
const resposta = await autorizarControle(`instalar a skill de ${caminho} mesmo assim`, { tipo: 'perigo' });
|
|
1023
|
+
if (resposta !== 'agora' && resposta !== 'sessao') return { ok: false, error: 'Skill não instalada.' };
|
|
1024
|
+
return skills.instalarDe(caminho, { forcar: true });
|
|
1025
|
+
}
|
|
1026
|
+
|
|
993
1027
|
async function sendPrompt(userText) {
|
|
994
1028
|
/* O portão do plano. Sem ele o bloqueio da abertura seria só um texto na
|
|
995
1029
|
tela: a pessoa leria "você não tem acesso" e mandaria a primeira coisa
|
|
@@ -1051,6 +1085,42 @@ async function enviarPedido(userText) {
|
|
|
1051
1085
|
|
|
1052
1086
|
const plan = prepararPasta(userText);
|
|
1053
1087
|
|
|
1088
|
+
// O Chrome só aparece na tela quando o pedido é de navegador (lib/browser.js, definirVisivel).
|
|
1089
|
+
require('../lib/browser.js').definirVisivel(querNavegador(userText));
|
|
1090
|
+
|
|
1091
|
+
/* AS REFERÊNCIAS DO VÍDEO, decididas pela pessoa antes de começar
|
|
1092
|
+
(lib/estudio/referencias.js): web aberta, só o escopo que ela der,
|
|
1093
|
+
ou nenhuma — só motion. No pedido único não há quem responda, e
|
|
1094
|
+
vale o aberto. */
|
|
1095
|
+
const referencias = require('../lib/estudio/referencias.js');
|
|
1096
|
+
referencias.limpar();
|
|
1097
|
+
const producao = require('../lib/estudio/diretor.js').precisaDeProducao(userText);
|
|
1098
|
+
if (producao.producao && producao.formato === 'video') {
|
|
1099
|
+
/* O modo sai do próprio pedido quando ele diz ("só motion", "só do
|
|
1100
|
+
site x.com", "web aberta"). Quando NÃO diz, pergunta antes de
|
|
1101
|
+
começar — "vídeo" às vezes quer dizer só o motion (02/10). No
|
|
1102
|
+
pedido único não há quem responda, e vale o aberto. */
|
|
1103
|
+
const r = referencias.inferir(userText);
|
|
1104
|
+
if (!r.explicito && process.stdin.isTTY && !state.semTerminal) {
|
|
1105
|
+
console.log('');
|
|
1106
|
+
console.log(' ' + c.brand('▸') + ' ' + c.bold(c.white('Que vídeo você quer?')));
|
|
1107
|
+
console.log(' ' + c.dim('1') + c.muted(' só motion — animação pura, nenhuma foto nem vídeo de fora'));
|
|
1108
|
+
console.log(' ' + c.dim('2') + c.muted(' motion + web aberta — ele pesquisa e busca imagem e vídeo à vontade'));
|
|
1109
|
+
console.log(' ' + c.dim('3') + c.muted(' motion + escopo fechado — só os sites e termos que você disser'));
|
|
1110
|
+
const dito = String(await ask(' ' + c.brand('❯ ') + c.muted('1, 2 ou 3 '))).trim();
|
|
1111
|
+
// Aqui 1 é o motion (a ordem da pergunta), não o 1 = aberto do entender().
|
|
1112
|
+
const modo = { 1: 'sem', 2: 'aberto', 3: 'fechado' }[(/^\s*([123])\b/.exec(dito) || [])[1]] || referencias.entender(dito) || 'aberto';
|
|
1113
|
+
r.modo = modo;
|
|
1114
|
+
r.escopo = modo === 'fechado'
|
|
1115
|
+
? String(await ask(' ' + c.brand('❯ ') + c.muted('quais sites ou termos? (separados por vírgula) '))).trim()
|
|
1116
|
+
: '';
|
|
1117
|
+
if (modo === 'fechado' && !r.escopo) r.modo = 'aberto';
|
|
1118
|
+
}
|
|
1119
|
+
referencias.definir(r.modo, r.escopo);
|
|
1120
|
+
console.log(c.dim(` ⎿ referências: ${{ aberto: 'web aberta', fechado: 'só ' + r.escopo, sem: 'nenhuma, só motion' }[r.modo]}`)
|
|
1121
|
+
+ (r.modo === 'aberto' ? c.dim(' · diga "só motion" ou "só do site x.com" para mudar') : ''));
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1054
1124
|
// O resumo da rodada anterior é feito em segundo plano logo depois
|
|
1055
1125
|
// dela; aqui só se espera ele terminar, para não sair com memória velha.
|
|
1056
1126
|
//
|
|
@@ -1117,7 +1187,8 @@ async function enviarPedido(userText) {
|
|
|
1117
1187
|
permissoes: state.config.permissoes,
|
|
1118
1188
|
// Pedido de mexer no computador leva a dica que libera as
|
|
1119
1189
|
// ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
|
|
1120
|
-
instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
|
|
1190
|
+
instrucao: [i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
|
|
1191
|
+
i === 0 ? require('../lib/estudio/referencias.js').instrucao() : ''].filter(Boolean).join('\n\n'),
|
|
1121
1192
|
});
|
|
1122
1193
|
|
|
1123
1194
|
r = await rodarPasso();
|
|
@@ -2164,7 +2235,7 @@ async function handleSlash(text) {
|
|
|
2164
2235
|
console.log(c.dim(' Ou abra o ') + c.brand('/marketplace') + c.dim(' e escolha "New Skill" para escrever uma.'));
|
|
2165
2236
|
break;
|
|
2166
2237
|
}
|
|
2167
|
-
const r =
|
|
2238
|
+
const r = await instalarComPortao(rest);
|
|
2168
2239
|
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
2169
2240
|
console.log(c.ok(` Skill instalada: ${r.nome}`) + c.muted(` (${shortPath(r.caminho)})`));
|
|
2170
2241
|
console.log(c.dim(' Já está ligada. /skills mostra todas.'));
|
|
@@ -2569,6 +2640,13 @@ async function main() {
|
|
|
2569
2640
|
if (process.argv[2] === 'video') {
|
|
2570
2641
|
process.exit(await require('../lib/video/cli.js').main(process.argv.slice(3)));
|
|
2571
2642
|
}
|
|
2643
|
+
// `primocode mcp …`: os servidores MCP pelo run_shell (lib/mcp.js).
|
|
2644
|
+
if (process.argv[2] === 'skill' || process.argv[2] === 'skills') {
|
|
2645
|
+
process.exit(require('../lib/skills.js').main(process.argv.slice(3)));
|
|
2646
|
+
}
|
|
2647
|
+
if (process.argv[2] === 'mcp') {
|
|
2648
|
+
process.exit(await require('../lib/mcp.js').main(process.argv.slice(3)));
|
|
2649
|
+
}
|
|
2572
2650
|
cfg.ensureStructure();
|
|
2573
2651
|
|
|
2574
2652
|
// Padrão fixo: ~/primocode, não importa de onde o comando foi dado. Antes ele
|
|
@@ -2626,7 +2704,9 @@ async function main() {
|
|
|
2626
2704
|
runAct próprio, e por isso ficava sem tudo o que vive lá: o plano em
|
|
2627
2705
|
passos, a retomada automática, o @arquivo, o checkpoint do /desfazer. */
|
|
2628
2706
|
state.semTerminal = true;
|
|
2707
|
+
global.__primocodeTarefa = promptText.slice(0, 60);
|
|
2629
2708
|
await sendPrompt(promptText);
|
|
2709
|
+
global.__primocodeTarefa = null;
|
|
2630
2710
|
process.exit(0);
|
|
2631
2711
|
}
|
|
2632
2712
|
|
|
@@ -2641,6 +2721,9 @@ async function main() {
|
|
|
2641
2721
|
comandos: comandosDoMenu,
|
|
2642
2722
|
vazio: 'peça alguma coisa, ou / para os comandos',
|
|
2643
2723
|
});
|
|
2724
|
+
// O spinner desenha dentro da caixa (lib/entrada.js mostrarSpinner).
|
|
2725
|
+
if (rl.tty) Spinner.caixa = rl;
|
|
2726
|
+
ui.titulo('PrimoCode');
|
|
2644
2727
|
|
|
2645
2728
|
rl.prompt();
|
|
2646
2729
|
|
|
@@ -2717,6 +2800,7 @@ async function main() {
|
|
|
2717
2800
|
while (queue.length) {
|
|
2718
2801
|
const text = queue.shift().trim();
|
|
2719
2802
|
if (!text) continue;
|
|
2803
|
+
ui.titulo(`⠋ PrimoCode — ${text}`);
|
|
2720
2804
|
try {
|
|
2721
2805
|
if (ehBash(text)) await rodarBash(text);
|
|
2722
2806
|
else if (isSlash(text)) await handleSlash(text);
|
|
@@ -2740,6 +2824,7 @@ async function main() {
|
|
|
2740
2824
|
busy = false;
|
|
2741
2825
|
state.trabalhando = false;
|
|
2742
2826
|
if (rl.trabalhando) rl.trabalhando(false);
|
|
2827
|
+
ui.titulo('PrimoCode');
|
|
2743
2828
|
// Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
|
|
2744
2829
|
// — senão ela aparece antes do resultado do último pedido.
|
|
2745
2830
|
if (closed) sayBye();
|
package/lib/act.js
CHANGED
|
@@ -31,6 +31,7 @@ const preLeitura = require('./pre-leitura');
|
|
|
31
31
|
// volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
|
|
32
32
|
// de um vídeo de dez cenas com pesquisa, e a pessoa lia "gastei as voltas".
|
|
33
33
|
const MAX_ITERATIONS = Number(process.env.PRIMOCODE_MAX_VOLTAS || 40);
|
|
34
|
+
const MAX_VOLTAS_AGENTE = Number(process.env.PRIMOCODE_MAX_VOLTAS_AGENTE || 20);
|
|
34
35
|
// Quantas vezes o agente é EMPURRADO quando para no meio para narrar
|
|
35
36
|
// ("agora vou criar o arquivo…") em vez de fazer. Ver `pareceParadaNoMeio`.
|
|
36
37
|
const MAX_EMPURROES = 3;
|
|
@@ -39,6 +40,10 @@ const MAX_RODADAS_PARADAS = 3;
|
|
|
39
40
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
40
41
|
// As leituras que, repetidas sem mudança no meio, devolvem o guardado.
|
|
41
42
|
const RELEITURA = new Set(['read_file', 'list_dir']);
|
|
43
|
+
/* O que só lê: pode rodar junto com as outras leituras da mesma rodada. */
|
|
44
|
+
const SO_LEITURA = new Set(['read_file', 'list_dir', 'glob', 'grep', 'pesquisar_referencia', 'estudio_site']);
|
|
45
|
+
const ehLeitura = (tc) => SO_LEITURA.has(tc.name)
|
|
46
|
+
|| (tc.name === 'run_shell' && /^\s*primocode\s+(skill|mcp\s+list|video\s+(help|get_timeline|get_transcript))\b/.test(String((tc.arguments && tc.arguments.command) || '')));
|
|
42
47
|
|
|
43
48
|
// ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
|
|
44
49
|
// O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
|
|
@@ -526,7 +531,7 @@ const TROCA_DE_ROTA = 'Você ficou três voltas sem conseguir rodar nada, repeti
|
|
|
526
531
|
+ '(write_todos) e faça AGORA o primeiro passo, por um caminho diferente: outra ferramenta, outro arquivo, '
|
|
527
532
|
+ 'outro comando. Se não houver caminho, chame finish e diga em uma frase o que impediu.';
|
|
528
533
|
|
|
529
|
-
function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
534
|
+
function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou, teto = MAX_ITERATIONS }) {
|
|
530
535
|
if (falhasPendentes.length) {
|
|
531
536
|
return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
|
|
532
537
|
}
|
|
@@ -534,11 +539,11 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
|
534
539
|
return 'Fiquei preso: tentei dois caminhos e os dois voltaram ao mesmo lugar, então parei antes de gastar sua cota à toa. '
|
|
535
540
|
+ 'O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
|
|
536
541
|
}
|
|
537
|
-
if (voltas >=
|
|
542
|
+
if (voltas >= teto) {
|
|
538
543
|
const emFalso = voltasEmFalso
|
|
539
544
|
? ` ${voltasEmFalso} ${voltasEmFalso === 1 ? 'chamada foi repetida' : 'chamadas foram repetidas'} e não rodaram de novo.`
|
|
540
545
|
: '';
|
|
541
|
-
return `Usei as ${
|
|
546
|
+
return `Usei as ${teto} voltas deste pedido sem chegar ao fim.${emFalso}`
|
|
542
547
|
+ ' O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
|
|
543
548
|
}
|
|
544
549
|
return 'Parei sem concluir e sem explicação do modelo. Diga "continua" que eu tento de novo daqui.';
|
|
@@ -632,7 +637,18 @@ const RITMO = '[ritmo]\n'
|
|
|
632
637
|
+ '- O resultado de write_file/edit_file é a verdade. Não releia um arquivo que você acabou de escrever para conferir (nem com cat/sed/head): a conferência é do PrimoCode.\n'
|
|
633
638
|
+ '- Várias ações independentes vão na MESMA resposta (ex.: escrever index.html, style.css e script.js juntos).\n'
|
|
634
639
|
+ '- Leia um arquivo inteiro uma vez, não em pedaços pequenos; não leia de novo o que não mudou.\n'
|
|
635
|
-
+ '- Não narre o próximo passo: faça. Termine com finish("o que foi feito")
|
|
640
|
+
+ '- Não narre o próximo passo: faça. Termine com finish("o que foi feito").\n'
|
|
641
|
+
+ '- VOCÊ DECIDE O CAMINHO. Pedido simples: faça direto. Pedido grande com partes independentes: write_todos com as partes, '
|
|
642
|
+
+ 'depois spawn_agent — um por parte, até 8 NA MESMA resposta (rodam juntos), cada um com o especialista e tudo que ele precisa saber. '
|
|
643
|
+
+ 'Cada um volta com uma ENTREGA: confira; o que falhou volta para conserto; a parte que depende de outra recebe a entrega dela no prompt.\n'
|
|
644
|
+
+ '- Antes de dizer pronto, PROVE: rode o teste ou o comando, abra o resultado, olhe a imagem. Sem prova, não está pronto.';
|
|
645
|
+
|
|
646
|
+
/* O ritmo do subagente: o mesmo, sem a parte de orquestrar (ele não abre outro agente). */
|
|
647
|
+
const RITMO_AGENTE = '[ritmo]\n'
|
|
648
|
+
+ '- O resultado de write_file/edit_file é a verdade. Não releia o que acabou de escrever.\n'
|
|
649
|
+
+ '- Leia um arquivo INTEIRO uma vez (read_file sem offset). Não leia aos pedaços, não leia de novo o que não mudou.\n'
|
|
650
|
+
+ '- Várias ações independentes na MESMA resposta.\n'
|
|
651
|
+
+ '- Faça o que a sua parte pede, prove (teste ou comando) e chame finish. Polimento além do pedido não é sua parte.';
|
|
636
652
|
|
|
637
653
|
/* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
|
|
638
654
|
entrar já lido (ver lib/pre-leitura.js). */
|
|
@@ -653,6 +669,11 @@ function buildMemoryContext(projectDir, projectName, pedido = '') {
|
|
|
653
669
|
const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
|
|
654
670
|
if (lidos) parts.push(lidos);
|
|
655
671
|
parts.push(RITMO);
|
|
672
|
+
// Os servidores MCP configurados, numa linha (lib/mcp.js).
|
|
673
|
+
try { const m = require('./mcp.js').dica(projectDir); if (m) parts.push(m); } catch { /* sem mcp */ }
|
|
674
|
+
// A equipe calada (lib/personas.js): o olhar do especialista que o pedido pede.
|
|
675
|
+
const equipe = require('./personas.js').bloco(pedido);
|
|
676
|
+
if (equipe) parts.push(equipe);
|
|
656
677
|
return parts.join('\n\n');
|
|
657
678
|
}
|
|
658
679
|
|
|
@@ -778,12 +799,9 @@ async function runAct(userPrompt, opts) {
|
|
|
778
799
|
* não é uma opção num plano limitado por token. */
|
|
779
800
|
let pedido = userPrompt;
|
|
780
801
|
if (!subagent) {
|
|
781
|
-
const comSkills = skills.
|
|
802
|
+
const comSkills = skills.cardapio(userPrompt);
|
|
782
803
|
pedido = comSkills.texto;
|
|
783
804
|
if (instrucao) pedido = `${instrucao}\n\nPEDIDO: ${pedido}`;
|
|
784
|
-
if (comSkills.skills.length) {
|
|
785
|
-
fala.pensamento(`seguindo: ${comSkills.skills.map((s) => s.titulo).join(', ')}`, profundidade);
|
|
786
|
-
}
|
|
787
805
|
}
|
|
788
806
|
let voltasEmFalso = 0;
|
|
789
807
|
// Pesquisas que voltaram de mãos vazias. A partir da terceira, a tool
|
|
@@ -807,6 +825,9 @@ async function runAct(userPrompt, opts) {
|
|
|
807
825
|
const memory = buildMemoryContext(projectDir, projectName, userPrompt);
|
|
808
826
|
if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
|
|
809
827
|
for (const m of historicoEnxuto(history)) messages.push(m);
|
|
828
|
+
} else {
|
|
829
|
+
// O subagente também trabalha no ritmo: sem isto ele relia o mesmo arquivo aos pedaços.
|
|
830
|
+
messages.push({ role: 'system', content: RITMO_AGENTE });
|
|
810
831
|
}
|
|
811
832
|
/* O que VAI para o modelo é `pedido` (com as skills na frente); o que
|
|
812
833
|
fica guardado na memória e no histórico é `userPrompt`, o que a pessoa
|
|
@@ -828,12 +849,21 @@ async function runAct(userPrompt, opts) {
|
|
|
828
849
|
* um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
|
|
829
850
|
if (!subagent && profundidade === 0) {
|
|
830
851
|
const diretor = require('./estudio/diretor.js');
|
|
852
|
+
// Vídeo que o Estúdio fez se edita no Remotion Studio, não no editor por linha do tempo.
|
|
853
|
+
const EDITAR_DO_ESTUDIO = 'SE ELA QUER EDITAR UM VÍDEO QUE O ESTÚDIO FEZ (não uma gravação dela): '
|
|
854
|
+
+ 'estudio_projeto acao:"editar" com o id do projeto abre o Remotion Studio no navegador; diga a pasta a ela e pare.\n';
|
|
831
855
|
const producao = diretor.precisaDeProducao(pedido);
|
|
832
856
|
if (diretor.precisaDeEdicao(pedido)) {
|
|
833
857
|
// A gravação da pessoa: o editor por linha do tempo (lib/video/), pelo run_shell.
|
|
834
|
-
messages.push({ role: 'system', content: 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
|
|
858
|
+
messages.push({ role: 'system', content: EDITAR_DO_ESTUDIO + 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
|
|
859
|
+
+ 'SE A PESSOA SÓ DISSE "edite esse vídeo" (sem dizer o quê), O VÍDEO É O PEDIDO: `primocode video import --json \'{"path":"<arquivo>"}\'`, '
|
|
860
|
+
+ 'depois `primocode video auto --json \'{}\'` (lê o que ela FALA na gravação e aplica cada efeito no momento em que diz), inspect, export e abra o mp4. '
|
|
861
|
+
+ 'Senão: '
|
|
835
862
|
+ 'rode `primocode video help` UMA vez (lista as operações), depois import → remove_silence/remove_words → add_captions → inspect (olhe o png) → export. '
|
|
836
|
-
+ 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo
|
|
863
|
+
+ 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo.\n'
|
|
864
|
+
+ 'EFEITOS que seguem a pessoa: separate_layers (recorte + momento 3D), replace_background (fundo novo no estalo), '
|
|
865
|
+
+ 'attach_3d (logo 3D na mão, segue a mão), merge_on_join (mãos juntas → fusão → cérebro), cut_silence_at (silêncio depois do estalo), '
|
|
866
|
+
+ 'style_captions (cor, legenda em cima da cabeça) — detalhe em `primocode video help efeitos`. Atalho: `primocode video do --json \'{"pedido":"<o pedido em português>"}\'`.' });
|
|
837
867
|
} else if (producao.producao) {
|
|
838
868
|
/* "SE ESTIVER NA SUA LISTA" não é hesitação — é a diferença entre
|
|
839
869
|
a tarefa seguir e a tarefa morrer.
|
|
@@ -854,8 +884,14 @@ async function runAct(userPrompt, opts) {
|
|
|
854
884
|
com as primitivas" — o modelo escrevia posição em pixel e o
|
|
855
885
|
vídeo saía quebrado, além de custar três ou quatro rodadas a
|
|
856
886
|
mais de modelo (medido em 25/09). */
|
|
887
|
+
/* A IA DECIDE (01/10): "pedi um roteiro de vídeo e ela já ia fazer o
|
|
888
|
+
motion". A regra só sabe que a palavra "vídeo" apareceu; quem sabe
|
|
889
|
+
se a pessoa quer o vídeo PRONTO ou só o roteiro, a ideia ou uma
|
|
890
|
+
resposta é o modelo. Então isto é o caminho SE for produzir, e não
|
|
891
|
+
mais uma ordem de produzir. */
|
|
857
892
|
messages.push({ role: 'system', content:
|
|
858
|
-
|
|
893
|
+
EDITAR_DO_ESTUDIO + 'VOCÊ DECIDE o que entregar, pelo que a pessoa pediu: se ela quer só o roteiro, a ideia, um texto ou uma resposta, ESCREVA isso e pare — não produza vídeo. '
|
|
894
|
+
+ `SE ela quer a peça pronta (formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}"), faça em três passos: `
|
|
859
895
|
+ `estudio_projeto acao:"criar" com formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}" → `
|
|
860
896
|
+ 'estudio_plano com "roteiro" (partes com tipo e fala; o PrimoCode faz layout, voz, ícones, música e sons) → '
|
|
861
897
|
+ 'estudio_render. Se a peça for de uma marca de verdade, estudio_site antes, e use as cores e a logo em roteiro.marca. '
|
|
@@ -906,10 +942,22 @@ async function runAct(userPrompt, opts) {
|
|
|
906
942
|
const MAX_ESPERAS_REDE = Number(process.env.PRIMOCODE_MAX_ESPERAS_REDE || 6);
|
|
907
943
|
|
|
908
944
|
let voltas = 0;
|
|
945
|
+
const teto = subagent ? MAX_VOLTAS_AGENTE : MAX_ITERATIONS;
|
|
946
|
+
let avisouTeto = false;
|
|
909
947
|
if (!subagent) TRABALHANDO = true;
|
|
910
948
|
try {
|
|
911
|
-
|
|
949
|
+
/* O AGENTE TEM TETO PRÓPRIO. No teste da loja (01/10) o "fechar
|
|
950
|
+
pendências do painel" passou de 38 voltas lendo o mesmo app.js aos
|
|
951
|
+
pedaços — a conversa dele inchou para 45 mil caracteres e uma resposta
|
|
952
|
+
levou 22 minutos. Uma parte de um plano cabe em 20 voltas; na 15ª ele
|
|
953
|
+
é avisado para fechar, e no teto volta com a entrega do que fez. */
|
|
954
|
+
for (let iteration = 0; iteration < teto && !PARAR; iteration++) {
|
|
912
955
|
voltas++;
|
|
956
|
+
if (subagent && !avisouTeto && voltas >= teto - 5) {
|
|
957
|
+
avisouTeto = true;
|
|
958
|
+
messages.push({ role: 'user', content: `Faltam ${teto - voltas + 1} voltas para o seu limite. Feche o essencial do que foi pedido agora, `
|
|
959
|
+
+ 'prove (rode o teste ou o comando) e chame finish dizendo o que ficou de fora. Nada de polimento extra.' });
|
|
960
|
+
}
|
|
913
961
|
// Recado digitado durante o passo anterior entra AGORA, antes da ida
|
|
914
962
|
// ao modelo — o resultado das ferramentas já está na conversa.
|
|
915
963
|
if (!subagent) entregarRecados(messages, profundidade);
|
|
@@ -1178,13 +1226,13 @@ async function runAct(userPrompt, opts) {
|
|
|
1178
1226
|
// fila — cada subagente só começava quando o anterior terminava, e a
|
|
1179
1227
|
// rodada custava a soma dos três. Agora as chamadas spawn_agent da
|
|
1180
1228
|
// rodada disparam de uma vez (Promise.all) e cada resultado volta na
|
|
1181
|
-
// posição certa da conversa. Limite de
|
|
1182
|
-
//
|
|
1229
|
+
// posição certa da conversa. Limite de 8 por rodada (era 4: o laço do
|
|
1230
|
+
// Qwen despacha várias direções de uma vez); o excedente roda em série.
|
|
1183
1231
|
const spawns = toolCalls.filter((tc) => tc.name === 'spawn_agent');
|
|
1184
1232
|
const resultadosSpawn = new Map();
|
|
1185
1233
|
if (spawns.length && !PARAR) garantirPasta();
|
|
1186
1234
|
if (spawns.length > 1 && !PARAR) {
|
|
1187
|
-
const juntos = spawns.slice(0,
|
|
1235
|
+
const juntos = spawns.slice(0, 8);
|
|
1188
1236
|
fala.paralelo(juntos.length, profundidade);
|
|
1189
1237
|
const rs = await Promise.all(juntos.map((tc) =>
|
|
1190
1238
|
runSubagent(tc.arguments || {}, {
|
|
@@ -1193,6 +1241,22 @@ async function runAct(userPrompt, opts) {
|
|
|
1193
1241
|
juntos.forEach((tc, i) => resultadosSpawn.set(tc.id, rs[i]));
|
|
1194
1242
|
}
|
|
1195
1243
|
|
|
1244
|
+
/* LEITURAS JUNTAS. O RITMO pede ao modelo para juntar as ações
|
|
1245
|
+
independentes numa resposta só — e ele junta: três read_file, um
|
|
1246
|
+
grep e uma pesquisa. Só que o laço abaixo rodava uma de cada vez, e
|
|
1247
|
+
a rodada custava a soma. O que só LÊ (não muda arquivo, tela nem
|
|
1248
|
+
conta) roda tudo de uma vez aqui; o laço usa o resultado pronto,
|
|
1249
|
+
passando pelos mesmos freios de sempre (repetição, registro). */
|
|
1250
|
+
const adiantadas = new Map();
|
|
1251
|
+
const leituras = toolCalls.filter(ehLeitura);
|
|
1252
|
+
if (leituras.length > 1 && !PARAR) {
|
|
1253
|
+
const rs = await Promise.all(leituras.slice(0, 8).map((tc) =>
|
|
1254
|
+
tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api })
|
|
1255
|
+
.catch((e) => ({ ok: false, error: e && e.message }))));
|
|
1256
|
+
leituras.slice(0, 8).forEach((tc, i) => adiantadas.set(tc.id, rs[i]));
|
|
1257
|
+
traco(`${adiantadas.size} leituras em paralelo`);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1196
1260
|
for (const tc of toolCalls) {
|
|
1197
1261
|
if (PARAR) break;
|
|
1198
1262
|
if (tc.name === 'finish') {
|
|
@@ -1234,7 +1298,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1234
1298
|
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
|
|
1235
1299
|
});
|
|
1236
1300
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1237
|
-
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
1301
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: r + lembreteDeParalelo(todosDaTarefa, spawns.length) });
|
|
1238
1302
|
continue;
|
|
1239
1303
|
}
|
|
1240
1304
|
|
|
@@ -1383,7 +1447,10 @@ async function runAct(userPrompt, opts) {
|
|
|
1383
1447
|
/* O servidor, o token e o cliente da API vão junto: a legenda do
|
|
1384
1448
|
estudio_video transcreve no servidor, e sem eles a ferramenta
|
|
1385
1449
|
respondia "a transcrição precisa do servidor". */
|
|
1386
|
-
|
|
1450
|
+
traco(`executando ${tc.name}`);
|
|
1451
|
+
const result = adiantadas.has(tc.id) ? adiantadas.get(tc.id)
|
|
1452
|
+
: await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api });
|
|
1453
|
+
traco(`${tc.name} devolveu ok=${result && result.ok}`);
|
|
1387
1454
|
logResult(tc.name, result, profundidade);
|
|
1388
1455
|
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1389
1456
|
if (RELEITURA.has(tc.name) && result && result.ok !== false) {
|
|
@@ -1511,7 +1578,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1511
1578
|
// não dá nem para saber que acabou. Medido: um pedido de navegador morria assim
|
|
1512
1579
|
// depois de a primeira tentativa de seletor falhar.
|
|
1513
1580
|
if (!finalText) {
|
|
1514
|
-
finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS });
|
|
1581
|
+
finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS, teto });
|
|
1515
1582
|
if (!subagent) fala.aviso(finalText);
|
|
1516
1583
|
}
|
|
1517
1584
|
|
|
@@ -1589,7 +1656,42 @@ async function runSubagent(args, ctx) {
|
|
|
1589
1656
|
|
|
1590
1657
|
const resumo = r.assistantMessage.content || 'sem resultado';
|
|
1591
1658
|
fala.agenteVoltou(resumo, profundidade);
|
|
1592
|
-
return resumo;
|
|
1659
|
+
return entregaDoAgente(especialistas.rotulo(esp), resumo, r.registro);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/* UM AGENTE DE CADA VEZ é o padrão do modelo grátis: no teste da loja
|
|
1663
|
+
(API + site + painel, 01/10) ele despachou os três em fila — 6 + 8 + 5
|
|
1664
|
+
minutos — quando o site e o painel só dependiam do contrato, que já
|
|
1665
|
+
existia. Quem decide continua sendo ele; a volta só lembra que as partes
|
|
1666
|
+
que faltam podem ir JUNTAS. Pura. */
|
|
1667
|
+
function lembreteDeParalelo(todos, naRodada) {
|
|
1668
|
+
const faltam = (todos || []).filter((t) => t && t.status !== 'completed' && t.status !== 'concluida');
|
|
1669
|
+
if (naRodada > 1 || faltam.length < 2) return '';
|
|
1670
|
+
return `\n\n(Faltam ${faltam.length} partes do plano. As que não dependem uma da outra vão JUNTAS: `
|
|
1671
|
+
+ 'vários spawn_agent na MESMA resposta rodam em paralelo.)';
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/* O SALTO ENTRE AGENTES. O pai recebia só o texto final do especialista —
|
|
1675
|
+
"pronto, fiz a API" — e, para passar a vez ao próximo (o do front, o
|
|
1676
|
+
revisor), tinha de adivinhar o que existia. Agora a volta é uma ENTREGA:
|
|
1677
|
+
quem fez, o que disse, os arquivos que nasceram e mudaram, os comandos, o
|
|
1678
|
+
que falhou. É o contexto que o próximo especialista precisa no prompt
|
|
1679
|
+
dele, e é o que deixa o pai mandar a falha de volta para conserto (o laço
|
|
1680
|
+
do Qwen: planeja → despacha → confere → conserta). Pura, para o teste. */
|
|
1681
|
+
function entregaDoAgente(quem, texto, reg) {
|
|
1682
|
+
const linhas = [`ENTREGA DO ${String(quem).toUpperCase()}`, String(texto).trim().slice(0, 4000)];
|
|
1683
|
+
if (reg) {
|
|
1684
|
+
const lista = (nome, xs) => { if (xs && xs.length) linhas.push(`${nome}: ${xs.slice(0, 15).join(', ')}`); };
|
|
1685
|
+
lista('criou', reg.criados);
|
|
1686
|
+
lista('alterou', reg.alterados);
|
|
1687
|
+
lista('rodou', (reg.comandos || []).map((x) => `${x.comando}${x.ok === false ? ` (saiu ${x.code})` : ''}`));
|
|
1688
|
+
lista('testes', (reg.testes || []).map((x) => `${x.comando}: ${x.ok ? 'passou' : 'FALHOU'}`));
|
|
1689
|
+
if (reg.falhas && reg.falhas.length) linhas.push('FALHOU: ' + reg.falhas.slice(0, 6).map((f) => `${f.acao} ${f.alvo}: ${f.motivo}`.slice(0, 200)).join(' | '));
|
|
1690
|
+
}
|
|
1691
|
+
linhas.push(reg && ((reg.falhas && reg.falhas.length) || (reg.testes || []).some((x) => !x.ok))
|
|
1692
|
+
? 'PRÓXIMO: há falha acima — mande de volta para conserto (spawn_agent com o erro no prompt) ou conserte você, antes de seguir.'
|
|
1693
|
+
: 'PRÓXIMO: se outra parte depende desta, passe a vez (spawn_agent) com esta entrega no prompt; senão, confira e siga.');
|
|
1694
|
+
return linhas.join('\n');
|
|
1593
1695
|
}
|
|
1594
1696
|
|
|
1595
1697
|
/* ══ O PIPELINE DE QUATRO PAPÉIS ═══════════════════════════════════════════
|
|
@@ -1702,5 +1804,5 @@ async function rodarPipeline(args, ctx) {
|
|
|
1702
1804
|
});
|
|
1703
1805
|
}
|
|
1704
1806
|
|
|
1705
|
-
module.exports = { runAct, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1807
|
+
module.exports = { runAct, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1706
1808
|
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|
package/lib/browser.js
CHANGED
|
@@ -23,9 +23,36 @@ const http = require('http');
|
|
|
23
23
|
const crypto = require('crypto');
|
|
24
24
|
const { spawn, execSync } = require('child_process');
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const PERFIL_VISIVEL = path.join(os.homedir(), '.primocode', 'chrome-profile');
|
|
27
27
|
// Dentro da faixa 3100-3199 do ambiente local, para o start.sh cuidar dela.
|
|
28
|
-
const
|
|
28
|
+
const PORTA_VISIVEL = Number(process.env.PRIMOCODE_CDP_PORT || 3199);
|
|
29
|
+
|
|
30
|
+
/* ── VISÍVEL SÓ QUANDO A PESSOA PEDIU ────────────────────────────────────
|
|
31
|
+
* "toda vez que eu mando alguma coisa pra ele ele abre umas 10.000 abas
|
|
32
|
+
* do Chrome — ele nem usa — e eu fecho e ele abre de novo" (Joel, 02/10)
|
|
33
|
+
* As ferramentas browser_* vão para o modelo em todo pedido, e ele as usa
|
|
34
|
+
* para PESQUISAR (o motion da Paris Group subiu o Chrome às 09:41 sem
|
|
35
|
+
* ninguém pedir navegador). Agora o terminal diz, por pedido, se a pessoa
|
|
36
|
+
* quer ver (definirVisivel): sem pedido de navegador, as mesmas ferramentas
|
|
37
|
+
* rodam num Chrome --headless de perfil e porta próprios, que nunca abre
|
|
38
|
+
* janela nem aba na tela — e morre com o processo. */
|
|
39
|
+
const PERFIL_ESCONDIDO = path.join(os.homedir(), '.primocode', 'chrome-escondido');
|
|
40
|
+
const PORTA_ESCONDIDA = Number(process.env.PRIMOCODE_CDP_PORT_ESCONDIDO || 3197);
|
|
41
|
+
let visivel = true;
|
|
42
|
+
let DEBUG_PORT = PORTA_VISIVEL;
|
|
43
|
+
let PROFILE_DIR = PERFIL_VISIVEL;
|
|
44
|
+
|
|
45
|
+
function definirVisivel(sim) {
|
|
46
|
+
const quer = sim !== false;
|
|
47
|
+
if (quer === visivel) return visivel;
|
|
48
|
+
visivel = quer;
|
|
49
|
+
DEBUG_PORT = quer ? PORTA_VISIVEL : PORTA_ESCONDIDA;
|
|
50
|
+
PROFILE_DIR = quer ? PERFIL_VISIVEL : PERFIL_ESCONDIDO;
|
|
51
|
+
// A sessão aberta é do outro navegador: a próxima ação conecta no certo.
|
|
52
|
+
if (currentSession) { try { currentSession.close(); } catch { /* já fechou */ } currentSession = null; }
|
|
53
|
+
return visivel;
|
|
54
|
+
}
|
|
55
|
+
function estaVisivel() { return visivel; }
|
|
29
56
|
|
|
30
57
|
// ---- Helpers ---------------------------------------------------
|
|
31
58
|
/* O Chromium que o Playwright baixa. Ele não fica no PATH nem em /Applications
|
|
@@ -414,12 +441,20 @@ async function ensureChromeRunning() {
|
|
|
414
441
|
args.push('--no-sandbox');
|
|
415
442
|
}
|
|
416
443
|
// Sem interface gráfica (CI, servidor): PRIMOCODE_BROWSER_HEADLESS=1.
|
|
417
|
-
|
|
418
|
-
|
|
444
|
+
// E sempre que a pessoa não pediu para ver (definirVisivel).
|
|
445
|
+
const escondido = !visivel;
|
|
446
|
+
if (escondido || process.env.PRIMOCODE_BROWSER_HEADLESS === '1' || process.env.PRIMOCODE_BROWSER_HEADLESS === 'true') {
|
|
447
|
+
args.push('--headless=new', '--disable-gpu', '--disable-dev-shm-usage', '--mute-audio');
|
|
419
448
|
}
|
|
420
449
|
args.push('about:blank');
|
|
421
450
|
chromeChild = spawn(chrome, args, { detached: true, stdio: 'ignore' });
|
|
422
451
|
chromeChild.on('error', () => {});
|
|
452
|
+
/* O escondido morre com o processo: ninguém o vê para fechar, e um
|
|
453
|
+
headless órfão por sessão era o que o revisor deixava em 24/09. */
|
|
454
|
+
if (escondido) {
|
|
455
|
+
const filho = chromeChild;
|
|
456
|
+
process.once('exit', () => { try { process.kill(-filho.pid); } catch { try { filho.kill(); } catch { /* já morreu */ } } });
|
|
457
|
+
}
|
|
423
458
|
chromeChild.unref();
|
|
424
459
|
|
|
425
460
|
// Espera a porta responder
|
|
@@ -492,6 +527,55 @@ async function open(url) {
|
|
|
492
527
|
return { ok: true, url };
|
|
493
528
|
}
|
|
494
529
|
|
|
530
|
+
/* ── ABRIR À VISTA ────────────────────────────────────────────────────────
|
|
531
|
+
* "Peço pra ele abrir o Chrome e o YouTube: em vez de só abrir, ele mostra
|
|
532
|
+
* o mouse dele abrindo o Chrome, pesquisando…" (30/09)
|
|
533
|
+
* O cursor do PrimoCode (voz/primo_voz/cursor.py, macOS) vai até a barra de
|
|
534
|
+
* endereço da janela de verdade, clica, digita o endereço letra por letra e
|
|
535
|
+
* aperta Enter — quem olha vê o computador sendo usado. A janela vem do CDP
|
|
536
|
+
* (Browser.getWindowForTarget), então o clique cai na barra mesmo com a
|
|
537
|
+
* janela em qualquer lugar da tela. Sem cursor (outro SO, sem permissão),
|
|
538
|
+
* navega direto, como antes. */
|
|
539
|
+
async function abrirVisivel(url) {
|
|
540
|
+
// Escondido não tem janela para o cursor ir: navega direto.
|
|
541
|
+
if (!visivel) return open(url);
|
|
542
|
+
await ensure();
|
|
543
|
+
const cursor = require('./cursor.js');
|
|
544
|
+
if (!verSeta() || !cursor.disponivel()) return open(url);
|
|
545
|
+
try { await currentSession.send('Page.bringToFront'); } catch { /* segue */ }
|
|
546
|
+
let janela = null;
|
|
547
|
+
try {
|
|
548
|
+
const alvo = await currentSession.send('Target.getTargetInfo');
|
|
549
|
+
const w = await currentSession.send('Browser.getWindowForTarget', alvo && alvo.targetInfo ? { targetId: alvo.targetInfo.targetId } : {});
|
|
550
|
+
janela = w && w.bounds;
|
|
551
|
+
} catch { janela = null; }
|
|
552
|
+
if (!janela || janela.left == null) return open(url);
|
|
553
|
+
// A barra fica uns 58 pt abaixo do topo da janela (abas + barra), a ~30% da largura.
|
|
554
|
+
const x = Math.round(janela.left + Math.min(420, janela.width * 0.3));
|
|
555
|
+
const y = Math.round(janela.top + 58);
|
|
556
|
+
const passos = [
|
|
557
|
+
['clicar', { x, y, botao: 'esquerdo', rotulo: 'barra de endereço' }],
|
|
558
|
+
['tecla', { combo: 'cmd+a' }],
|
|
559
|
+
['digitar', { texto: url.replace(/^https?:\/\//, ''), rotulo: 'digitando o endereço' }],
|
|
560
|
+
['tecla', { combo: 'enter' }],
|
|
561
|
+
];
|
|
562
|
+
for (const [cmd, args] of passos) {
|
|
563
|
+
const r = await cursor.mandar(cmd, args);
|
|
564
|
+
if (!r || r.ok === false) return open(url); // sem permissão de Acessibilidade: navega direto
|
|
565
|
+
}
|
|
566
|
+
const deadline = Date.now() + 20000;
|
|
567
|
+
await sleep(600);
|
|
568
|
+
while (Date.now() < deadline) {
|
|
569
|
+
try {
|
|
570
|
+
const r = await currentSession.send('Runtime.evaluate', { expression: 'document.readyState + "|" + location.href', returnByValue: true });
|
|
571
|
+
const [estado, href] = String((r.result && r.result.value) || '').split('|');
|
|
572
|
+
if (estado === 'complete' && href && href !== 'about:blank') return { ok: true, url: href, visivel: true };
|
|
573
|
+
} catch { /* a página está trocando */ }
|
|
574
|
+
await sleep(200);
|
|
575
|
+
}
|
|
576
|
+
return { ok: true, url, visivel: true };
|
|
577
|
+
}
|
|
578
|
+
|
|
495
579
|
async function evaluate(js) {
|
|
496
580
|
await ensure();
|
|
497
581
|
const r = await currentSession.send('Runtime.evaluate', {
|
|
@@ -1207,7 +1291,7 @@ function fecharRevisor() {
|
|
|
1207
1291
|
}
|
|
1208
1292
|
|
|
1209
1293
|
module.exports = {
|
|
1210
|
-
retrato,
|
|
1294
|
+
retrato, abrirVisivel, definirVisivel, estaVisivel,
|
|
1211
1295
|
SEM_KEYCHAIN,
|
|
1212
1296
|
scriptDaSeta, verSeta, findChrome,
|
|
1213
1297
|
open, click, clickText, clickSelector, type, typeInSelector,
|
package/lib/catalogo.js
CHANGED
|
@@ -77,7 +77,9 @@ const GRUPOS = [
|
|
|
77
77
|
resumo: 'vídeo, motion, slides e arte — a IA faz, e no fim abre o resultado pronto',
|
|
78
78
|
itens: [
|
|
79
79
|
{ tool: 'estudio_projeto', escreve: true, faz: 'abre, acha e lista os projetos do estúdio — cada peça é uma pasta que não some',
|
|
80
|
-
args: 'acao: criar | abrir | procurar | listar | anotar. '
|
|
80
|
+
args: 'acao: criar | abrir | procurar | listar | anotar | editar. '
|
|
81
|
+
+ 'editar: id (sem id, o mais novo) — quando o usuário quer EDITAR um vídeo que o Estúdio fez, '
|
|
82
|
+
+ 'abre o projeto no Remotion Studio (o editor, no navegador) e devolve a pasta. '
|
|
81
83
|
+ 'criar: titulo, pedido, formato (video|slides|arte). '
|
|
82
84
|
+ 'abrir: id — devolve o plano, o CADERNO (o que foi decidido e por quê) e os arquivos. '
|
|
83
85
|
+ 'procurar: texto — acha pelo que o usuário lembra ("aquele do PicPay"), inclusive por detalhe do meio do processo. '
|
|
@@ -89,8 +91,9 @@ const GRUPOS = [
|
|
|
89
91
|
+ 'Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens} passos{titulo,itens} '
|
|
90
92
|
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
|
|
91
93
|
+ 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). '
|
|
92
|
-
+ '
|
|
93
|
-
+ '
|
|
94
|
+
+ 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
|
|
95
|
+
+ 'estilo: hibrido (PADRÃO: foto e vídeo reais que não param + o motion por cima; dê busca:"termo em inglês" por parte para o b-roll) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; é o do modo sem referências e o de quem pede motion/animação; com modelo3d o produto 3D entra numa batida) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
96
|
+
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|lavanda|creme ou tema claro|escuro no roteiro) | impacto | cinema. '
|
|
94
97
|
+ 'marca e lancamento aceitam visual:orbe|vetor|estrela|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
|
|
95
98
|
+ 'voz (thalita|antonio|francisca). Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
|
96
99
|
+ 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '
|