primocode 10.0.0 → 10.0.9
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 +2 -2
- package/bin/primocode.js +69 -1
- package/lib/act.js +118 -19
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +1 -1
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/entrada.js +73 -9
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +23 -8
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +244 -36
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +3 -1
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit.js +7 -1
- package/lib/estudio/lancamento.js +28 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +53 -9
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/skills.js +36 -0
- package/lib/tools.js +22 -3
- 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/motion-de-primeira/SKILL.md +5 -4
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
|
@@ -32,6 +32,14 @@ const EFEITOS = {
|
|
|
32
32
|
subida: ['-f', 'lavfi', '-i', "aevalsrc='0.28*sin(2*PI*(180+260*t*t)*t)*(t/1.4)':s=44100:d=1.4", '-af', 'afade=t=out:st=1.2:d=0.2'],
|
|
33
33
|
// Toque grave e macio: o que chega e assenta.
|
|
34
34
|
toque: ['-f', 'lavfi', '-i', "aevalsrc='0.7*sin(2*PI*(110-50*t)*t)*exp(-7*t)':s=44100:d=0.6", '-af', 'lowpass=f=900'],
|
|
35
|
+
/* Os CHEIOS do motion gerativo ("motion é vida: efeito sonoro NA ação",
|
|
36
|
+
30/09): o whoosh da passagem, o impacto da tese, o riser antes da
|
|
37
|
+
revelação e o pop da letra que nasce. Ainda sintetizados, ainda limpos. */
|
|
38
|
+
whoosh: ['-f', 'lavfi', '-i', 'anoisesrc=d=0.75:c=pink:a=0.9', '-af',
|
|
39
|
+
'bandpass=f=1100:width_type=o:w=2.2,afade=t=in:st=0:d=0.42:curve=exp,afade=t=out:st=0.42:d=0.33:curve=qsin,volume=2.2'],
|
|
40
|
+
impacto: ['-f', 'lavfi', '-i', "aevalsrc='0.95*sin(2*PI*(62-26*t)*t)*exp(-4.5*t)+0.45*(random(0)*2-1)*exp(-38*t)':s=44100:d=1.1", '-af', 'lowpass=f=2600,volume=1.4'],
|
|
41
|
+
riser: ['-f', 'lavfi', '-i', "aevalsrc='(0.32*sin(2*PI*(170+620*t*t)*t)+0.22*(random(0)*2-1))*pow(t/1.1,2)':s=44100:d=1.1", '-af', 'highpass=f=260,afade=t=out:st=1.04:d=0.06,volume=1.3'],
|
|
42
|
+
pop: ['-f', 'lavfi', '-i', "aevalsrc='0.8*sin(2*PI*(980-640*t)*t)*exp(-28*t)*(1-exp(-700*t))':s=44100:d=0.18"],
|
|
35
43
|
};
|
|
36
44
|
|
|
37
45
|
/**
|
|
@@ -59,4 +67,33 @@ function preparar(pasta, { rodar = null } = {}) {
|
|
|
59
67
|
return sons;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
/**
|
|
71
|
+
* A família de sons de UM vídeo (gerativo.js sonsDoVideo): cada receita vira
|
|
72
|
+
* midia/sfx-g<semente>-<nome>.mp3. O que já existe não é refeito.
|
|
73
|
+
*/
|
|
74
|
+
function prepararReceitas(pasta, { receitas = {}, semente = 0 } = {}, { rodar = null } = {}) {
|
|
75
|
+
const exec = rodar || ((args) => {
|
|
76
|
+
try { require('child_process').execFileSync('ffmpeg', args, { stdio: 'ignore', timeout: 30000 }); return true; }
|
|
77
|
+
catch { return false; }
|
|
78
|
+
});
|
|
79
|
+
const sons = {};
|
|
80
|
+
const sufixo = (semente >>> 0).toString(36);
|
|
81
|
+
for (const [nome, args] of Object.entries(receitas)) {
|
|
82
|
+
const rel = `midia/sfx-g${sufixo}-${nome}.mp3`;
|
|
83
|
+
const cheio = path.join(pasta, rel);
|
|
84
|
+
if (!fs.existsSync(cheio)) {
|
|
85
|
+
fs.mkdirSync(path.dirname(cheio), { recursive: true });
|
|
86
|
+
// O limitador no fim: o golpe sintetizado passava de 0 dB e estalava.
|
|
87
|
+
// "{pasta}" é a pasta do projeto: o som feito da própria trilha lê o arquivo dela.
|
|
88
|
+
const a = args.map((x) => String(x).split('{pasta}').join(pasta));
|
|
89
|
+
const fc = a.indexOf('-filter_complex'), i = a.indexOf('-af');
|
|
90
|
+
if (fc >= 0) a[fc + 1] += ',alimiter=limit=0.8:level=false';
|
|
91
|
+
else if (i >= 0) a[i + 1] += ',alimiter=limit=0.8:level=false'; else a.push('-af', 'alimiter=limit=0.8:level=false');
|
|
92
|
+
if (!exec(['-y', '-loglevel', 'error', ...a, '-q:a', '3', cheio])) continue;
|
|
93
|
+
}
|
|
94
|
+
sons[nome] = rel;
|
|
95
|
+
}
|
|
96
|
+
return sons;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { EFEITOS, preparar, prepararReceitas };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* variedade.js — a trilha e os efeitos dos vídeos de corte (híbrido, cinema,
|
|
3
|
+
* impacto) sem repetir de um vídeo para o outro.
|
|
4
|
+
*
|
|
5
|
+
* "todos os vídeos é sempre as mesmas músicas, os mesmos efeitos sonoros,
|
|
6
|
+
* sempre o mesmo padrão de vídeo" (Joel, 02/10)
|
|
7
|
+
*
|
|
8
|
+
* A causa era a mesma nos dois: um termo fixo na busca ("inspiring
|
|
9
|
+
* corporate", "cinematic whoosh") e o PRIMEIRO resultado baixado. A busca
|
|
10
|
+
* devolve a mesma lista na mesma ordem, então todo vídeo saía com a mesma
|
|
11
|
+
* faixa e o mesmo whoosh. Aqui:
|
|
12
|
+
* 1. o termo sai de um cardápio pelo clima do roteiro, em ordem sorteada;
|
|
13
|
+
* 2. dos resultados, sorteia — e pula o que os últimos vídeos já usaram;
|
|
14
|
+
* 3. o que foi usado entra na memória (~/.primocode/studio/<tipo>-usadas.json).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const PASTA = path.join(os.homedir(), '.primocode', 'studio');
|
|
24
|
+
const MEMORIA = { trilha: 16, sfx: 40 };
|
|
25
|
+
|
|
26
|
+
function arquivo(tipo) { return path.join(PASTA, `${tipo}-usadas.json`); }
|
|
27
|
+
|
|
28
|
+
function usadas(tipo) {
|
|
29
|
+
try { const l = JSON.parse(fs.readFileSync(arquivo(tipo), 'utf8')); return Array.isArray(l) ? l : []; } catch { return []; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function marcar(tipo, id) {
|
|
33
|
+
if (!id) return;
|
|
34
|
+
try {
|
|
35
|
+
fs.mkdirSync(PASTA, { recursive: true });
|
|
36
|
+
const l = [id, ...usadas(tipo).filter((x) => x !== id)].slice(0, MEMORIA[tipo] || 20);
|
|
37
|
+
fs.writeFileSync(arquivo(tipo), JSON.stringify(l));
|
|
38
|
+
} catch { /* sem memória: só perde a garantia de não repetir */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function embaralhar(lista, aleatorio = Math.random) {
|
|
42
|
+
const a = [...lista];
|
|
43
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
44
|
+
const j = Math.floor(aleatorio() * (i + 1));
|
|
45
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
46
|
+
}
|
|
47
|
+
return a;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Os itens da busca na ordem de tentar: os nunca usados primeiro (sorteados), os usados no fim. */
|
|
51
|
+
function ordenar(itens, tipo, aleatorio = Math.random) {
|
|
52
|
+
const ja = new Set(usadas(tipo));
|
|
53
|
+
const validos = (itens || []).filter((x) => x && x.url);
|
|
54
|
+
return [...embaralhar(validos.filter((x) => !ja.has(x.url)), aleatorio), ...validos.filter((x) => ja.has(x.url))];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── a trilha: termos por clima ──────────────────────────────────────────
|
|
58
|
+
const CLIMAS = {
|
|
59
|
+
tecnologia: ['electronic instrumental', 'synthwave instrumental', 'future bass instrumental', 'downtempo electronic', 'minimal techno instrumental', 'chillwave instrumental'],
|
|
60
|
+
energetico: ['funk instrumental', 'rock instrumental', 'drum and bass instrumental', 'breakbeat instrumental', 'upbeat indie instrumental', 'house instrumental'],
|
|
61
|
+
epico: ['epic orchestral', 'trailer instrumental', 'cinematic drums', 'epic hybrid instrumental', 'dramatic strings'],
|
|
62
|
+
calmo: ['lofi hip hop instrumental', 'ambient instrumental', 'acoustic guitar instrumental', 'piano instrumental', 'jazz instrumental'],
|
|
63
|
+
alegre: ['ukulele instrumental', 'indie pop instrumental', 'happy acoustic instrumental', 'latin instrumental', 'disco instrumental'],
|
|
64
|
+
emocional: ['piano emotional instrumental', 'post rock instrumental', 'cinematic strings', 'neo classical instrumental'],
|
|
65
|
+
misterio: ['dark ambient instrumental', 'trip hop instrumental', 'suspense instrumental', 'noir jazz instrumental'],
|
|
66
|
+
neutro: ['indie instrumental', 'soul instrumental', 'jazz hop instrumental', 'world instrumental', 'electronic instrumental', 'acoustic instrumental'],
|
|
67
|
+
};
|
|
68
|
+
const SINONIMOS = {
|
|
69
|
+
tecnologia: /tec|tech|ia\b|ai\b|software|app|digital|futur/, energetico: /energ|agit|rapid|forte|esport|academ/,
|
|
70
|
+
epico: /epic|epico|grandi|trailer|impact/, calmo: /calm|tranq|suave|relax|chill/, alegre: /aleg|feliz|divert|leve/,
|
|
71
|
+
emocional: /emoc|emoti|inspir|sentim|historia/, misterio: /mist|sombr|dark|suspen|tens/,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function climaDe(roteiro = {}, estilo = '') {
|
|
75
|
+
const t = String([roteiro.clima, roteiro.genero, roteiro.tom].filter(Boolean).join(' ')).toLowerCase()
|
|
76
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
77
|
+
for (const [clima, re] of Object.entries(SINONIMOS)) if (re.test(t)) return clima;
|
|
78
|
+
return estilo === 'impacto' ? 'energetico' : 'neutro';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Os termos para buscar a trilha, em ordem de tentar. O pedido explícito
|
|
83
|
+
* (roteiro.musica) vem primeiro; depois o clima, sorteado; e por último um
|
|
84
|
+
* clima vizinho, para nunca sair sem música.
|
|
85
|
+
*/
|
|
86
|
+
function termosTrilha(roteiro = {}, estilo = '', aleatorio = Math.random) {
|
|
87
|
+
const clima = climaDe(roteiro, estilo);
|
|
88
|
+
const reserva = clima === 'neutro' ? 'calmo' : 'neutro';
|
|
89
|
+
return [...new Set([roteiro.musica, ...embaralhar(CLIMAS[clima], aleatorio).slice(0, 3),
|
|
90
|
+
...embaralhar(CLIMAS[reserva], aleatorio).slice(0, 2)].filter(Boolean))];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── os efeitos: várias formas de pedir o mesmo som ───────────────────────
|
|
94
|
+
const SFX = {
|
|
95
|
+
teclado: ['keyboard typing', 'typing keyboard', 'mechanical keyboard', 'laptop typing', 'typewriter keys'],
|
|
96
|
+
whoosh: ['cinematic whoosh', 'swoosh transition', 'air whoosh', 'fast swipe', 'woosh pass by', 'riser whoosh'],
|
|
97
|
+
impacto: ['cinematic boom impact', 'deep cinematic impact', 'trailer hit', 'bass drop hit', 'punch hit', 'sub boom'],
|
|
98
|
+
sala: ['crowd murmur indoor', 'office ambience', 'people talking background', 'cafe ambience', 'room tone people'],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function termosSfx(nome, aleatorio = Math.random) {
|
|
102
|
+
return embaralhar(SFX[nome] || [nome], aleatorio).slice(0, 3);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { usadas, marcar, embaralhar, ordenar, climaDe, termosTrilha, termosSfx, CLIMAS, SFX };
|
package/lib/estudio/vox.js
CHANGED
|
@@ -91,7 +91,11 @@ function montarVox({ roteiro, largura = 1920, altura = 1080, tempos = [], narrac
|
|
|
91
91
|
const partes = (roteiro && roteiro.partes) || [];
|
|
92
92
|
if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
|
|
93
93
|
const W = largura, H = altura, deitado = W >= H;
|
|
94
|
-
|
|
94
|
+
/* Deitado, o quadro tem 1920 de largura e o mesmo 1080 de altura do em
|
|
95
|
+
pé: com a escala do lado menor, tudo saía do tamanho do vertical,
|
|
96
|
+
miúdo e solto num canvas vazio ("deixar os elementos mais organizados",
|
|
97
|
+
29/09). No 16:9 a tipografia cresce 30%. */
|
|
98
|
+
const b = (Math.min(W, H) / 1080) * (deitado ? 1.3 : 1);
|
|
95
99
|
const M = Math.round(Math.min(W, H) * 0.075);
|
|
96
100
|
const TOPO = Math.round(H * (deitado ? 0.17 : 0.2)); // a banda intocada
|
|
97
101
|
|
|
@@ -115,7 +119,8 @@ function montarVox({ roteiro, largura = 1920, altura = 1080, tempos = [], narrac
|
|
|
115
119
|
const leitura = (p) => 1.6 + 0.3 * String([p.titulo, p.sub, p.valor, p.rotulo, p.nao, p.sim, ...itensDe(p)].filter(Boolean).join(' ')).split(/\s+/).length;
|
|
116
120
|
const { ini, dur, total, temVoz } = tempoDasPartes(partes, tempos, { semVoz: leitura });
|
|
117
121
|
const efeitos = [];
|
|
118
|
-
|
|
122
|
+
// Efeito que não se ouve é efeito que não existe: 1,6× o volume antigo.
|
|
123
|
+
const som = (t, nome, volume) => { if (sons[nome]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome], volume: Math.min(0.65, volume * 1.6) }); };
|
|
119
124
|
|
|
120
125
|
// ── as quatro profundidades, e o que mora em cada uma ──────────────
|
|
121
126
|
const CAMADAS = [{ p: 0.12, desfoque: 2.2 }, { p: 0.4, desfoque: 0.9 }, { p: 0.55, desfoque: 0, sujeito: true }, { p: 1.25, desfoque: 3.4 }];
|
|
@@ -133,10 +138,16 @@ function montarVox({ roteiro, largura = 1920, altura = 1080, tempos = [], narrac
|
|
|
133
138
|
return { zoom: [k(0, 3.4), k(Math.min(1.8, d * 0.55), 1.0, 'camera'), k(fimLento, 1.03, 'camera'), k(d, 1.06, 'acelera')],
|
|
134
139
|
x: [k(0, 0), k(d, lado * 10 * b, 'camera')] };
|
|
135
140
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
/* "Mais movimento" (29/09): a câmera contida do guia VOX (4,5% de
|
|
142
|
+
zoom, 14 px) lia como tela parada. Deitado ela empurra 12% (22% no
|
|
143
|
+
clímax) e desliza 34 px; em pé, menos, porque a âncora ocupa a
|
|
144
|
+
largura toda. */
|
|
145
|
+
const z1 = modo === 'climax' ? (deitado ? 1.22 : 1.16) : (deitado ? 1.12 : 1.08);
|
|
146
|
+
const desliza = (deitado ? 34 : 22) * b; // mais que isso leva a âncora até a borda
|
|
147
|
+
return { zoom: [k(0, 1.0), k(fimLento, z1, 'camera'), k(d, z1 + 0.05, 'acelera')],
|
|
148
|
+
x: [k(0, -lado * desliza), k(d, lado * desliza, 'camera')],
|
|
149
|
+
y: [k(0, lado * desliza * 0.35), k(d, -lado * desliza * 0.35, 'camera')],
|
|
150
|
+
rotacao: [k(0, -lado * 0.7), k(d, lado * 0.7, 'camera')] };
|
|
140
151
|
};
|
|
141
152
|
|
|
142
153
|
/* O CORTE NO BEAT dentro da parte longa: passou de 5 s, a câmera pula de
|
|
@@ -221,8 +232,10 @@ function montarVox({ roteiro, largura = 1920, altura = 1080, tempos = [], narrac
|
|
|
221
232
|
const maior = itens.reduce((a, x) => ((x.termo + (x.def ? ' — ' + x.def : '')).length > a.length ? x.termo + (x.def ? ' — ' + x.def : '') : a), '');
|
|
222
233
|
// Uma linha por item quando cabe grande; senão, duas.
|
|
223
234
|
const umaLinha = caber(maior, { largura: larguraAncora * (deitado ? 1.05 : 1), linhas: 1, max: (tipo === 'contraste' ? 170 : itens.length > 3 ? 80 : 96) * b, min: 30 * b, fonte: F.titulo });
|
|
224
|
-
|
|
225
|
-
|
|
235
|
+
// O contraste com frase longa saía numa linha só, miúdo no meio do
|
|
236
|
+
// quadro (29/09). Não coube grande em uma? Vai em duas, maior.
|
|
237
|
+
const tam = umaLinha.tamanho >= (tipo === 'contraste' ? 72 : 58) * b ? umaLinha.tamanho
|
|
238
|
+
: caber(maior, { largura: larguraAncora * (deitado ? 1.05 : 1), linhas: 2, max: (tipo === 'contraste' ? 120 : 80) * b, min: 30 * b, fonte: F.titulo }).tamanho;
|
|
226
239
|
const ts = g(itens.map((x) => (tipo === 'passos' ? x.def : x.termo)), { t0: titulo && tipo !== 'contraste' ? 0.6 : 0.3 });
|
|
227
240
|
itens.forEach((x, j) => { x.t = ts[j]; });
|
|
228
241
|
const linhasEst = itens.reduce((n, x) => n + Math.ceil(((x.termo + (x.def ? ' — ' + x.def : '')).length * tam * 0.56) / larguraAncora), 0);
|
|
@@ -354,6 +367,22 @@ function montarVox({ roteiro, largura = 1920, altura = 1080, tempos = [], narrac
|
|
|
354
367
|
}
|
|
355
368
|
|
|
356
369
|
const cam = corteInterno(camera(d, modo, i), d, tp, t0);
|
|
370
|
+
/* CADA ELEMENTO RESPIRA: além da câmera, cada peça flutua um pouco na
|
|
371
|
+
direção dela (alternando) e cresce 3% — em profundidades diferentes
|
|
372
|
+
isso vira parallax. O que já tem animação própria de posição,
|
|
373
|
+
escala ou rotação fica como está. */
|
|
374
|
+
els.forEach((el, j) => {
|
|
375
|
+
if (!el || el.peca === 'papel') return;
|
|
376
|
+
const a = el.anima || {};
|
|
377
|
+
const sinal = (i + j) % 2 ? 1 : -1;
|
|
378
|
+
const amp = (deitado ? 14 : 10) * b * (1 + (j % 3) * 0.4);
|
|
379
|
+
el.anima = {
|
|
380
|
+
...a,
|
|
381
|
+
...(a.y ? {} : { y: [k(0, sinal * amp), k(d, -sinal * amp, 'camera')] }),
|
|
382
|
+
...(a.escala ? {} : { escala: [k(0, 1), k(d, 1.03, 'camera')] }),
|
|
383
|
+
...(a.rotacao ? {} : { rotacao: [k(0, -sinal * 0.4), k(d, sinal * 0.4, 'camera')] }),
|
|
384
|
+
};
|
|
385
|
+
});
|
|
357
386
|
return {
|
|
358
387
|
duracao: d,
|
|
359
388
|
fundo: escuro ? ESCURO : PAPEL,
|
package/lib/mcp.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp.js — os servidores MCP ao alcance do agente, sem mudar o servidor.
|
|
3
|
+
*
|
|
4
|
+
* "Coloca MCP, coloca coisas pra otimizar ele." (30/09)
|
|
5
|
+
*
|
|
6
|
+
* Quem declara as ferramentas ao modelo é o servidor do PrimoCode, que este
|
|
7
|
+
* cliente não muda. Então o MCP entra como o editor de vídeo entrou: um
|
|
8
|
+
* subcomando que se descreve (`primocode mcp list`, `primocode mcp call`), e
|
|
9
|
+
* o agente chama pelo run_shell. A lista curta dos servidores e das tools vai
|
|
10
|
+
* no contexto — guardada em cache, para não subir servidor a cada pedido.
|
|
11
|
+
*
|
|
12
|
+
* A configuração é a mesma do Claude Code, para quem já tem uma:
|
|
13
|
+
* ~/.primocode/mcp.json e <pasta do projeto>/.mcp.json
|
|
14
|
+
* { "mcpServers": { "nome": { "command": "npx", "args": [...], "env": {...} } } }
|
|
15
|
+
*
|
|
16
|
+
* O cliente é o mínimo do protocolo por stdio: JSON-RPC 2.0, uma mensagem por
|
|
17
|
+
* linha — initialize, notifications/initialized, tools/list, tools/call.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { spawn } = require('child_process');
|
|
26
|
+
|
|
27
|
+
const CASA = path.join(os.homedir(), '.primocode');
|
|
28
|
+
const CONFIG = path.join(CASA, 'mcp.json');
|
|
29
|
+
const CACHE = path.join(CASA, 'mcp-cache.json');
|
|
30
|
+
|
|
31
|
+
function lerJson(arq) { try { return JSON.parse(fs.readFileSync(arq, 'utf8')); } catch { return null; } }
|
|
32
|
+
|
|
33
|
+
/* O PADRÃO: o Context7, a documentação ATUAL de qualquer biblioteca. É o
|
|
34
|
+
que mais melhora a programação de um modelo grátis — ele para de chutar a
|
|
35
|
+
API de memória. Só sobe quando o agente chama; PRIMOCODE_SEM_MCP=1 tira. */
|
|
36
|
+
const PADRAO = { context7: { command: 'npx', args: ['-y', '@upstash/context7-mcp@latest'] } };
|
|
37
|
+
|
|
38
|
+
/** Os servidores configurados: o padrão, o do usuário e o do projeto (o do projeto vence). */
|
|
39
|
+
function servidores(pasta = process.cwd()) {
|
|
40
|
+
const out = process.env.PRIMOCODE_SEM_MCP === '1' ? {} : { ...PADRAO };
|
|
41
|
+
for (const arq of [CONFIG, path.join(pasta, '.mcp.json')]) {
|
|
42
|
+
const j = lerJson(arq);
|
|
43
|
+
for (const [nome, s] of Object.entries((j && j.mcpServers) || {})) if (s && s.command) out[nome] = s;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Abre um servidor, faz o aperto de mão e devolve {pedir, fechar}. */
|
|
49
|
+
function conectar(def, { tetoMs = 60000 } = {}) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const p = spawn(def.command, def.args || [], { env: { ...process.env, ...(def.env || {}) }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
52
|
+
let buf = '', id = 0;
|
|
53
|
+
const espera = new Map();
|
|
54
|
+
const relogio = setTimeout(() => { try { p.kill(); } catch {} reject(new Error('o servidor MCP não respondeu')); }, tetoMs);
|
|
55
|
+
p.on('error', (e) => { clearTimeout(relogio); reject(e); });
|
|
56
|
+
p.stdout.on('data', (d) => {
|
|
57
|
+
buf += d.toString();
|
|
58
|
+
let i;
|
|
59
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
60
|
+
const linha = buf.slice(0, i).trim(); buf = buf.slice(i + 1);
|
|
61
|
+
if (!linha) continue;
|
|
62
|
+
let m; try { m = JSON.parse(linha); } catch { continue; }
|
|
63
|
+
if (m.id != null && espera.has(m.id)) { const w = espera.get(m.id); espera.delete(m.id); m.error ? w.rej(new Error(m.error.message || 'erro do MCP')) : w.res(m.result); }
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const pedir = (method, params = {}) => new Promise((res, rej) => {
|
|
67
|
+
const meu = ++id;
|
|
68
|
+
espera.set(meu, { res, rej });
|
|
69
|
+
p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: meu, method, params }) + '\n');
|
|
70
|
+
});
|
|
71
|
+
const avisar = (method, params = {}) => p.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
|
|
72
|
+
pedir('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'primocode', version: '10' } })
|
|
73
|
+
.then(() => { avisar('notifications/initialized'); clearTimeout(relogio); resolve({ pedir, fechar: () => { try { p.kill(); } catch {} } }); })
|
|
74
|
+
.catch((e) => { clearTimeout(relogio); try { p.kill(); } catch {} reject(e); });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function listar(pasta) {
|
|
79
|
+
const lista = servidores(pasta);
|
|
80
|
+
const out = {};
|
|
81
|
+
await Promise.all(Object.entries(lista).map(async ([nome, def]) => {
|
|
82
|
+
try {
|
|
83
|
+
const c = await conectar(def);
|
|
84
|
+
const r = await c.pedir('tools/list');
|
|
85
|
+
c.fechar();
|
|
86
|
+
out[nome] = (r.tools || []).map((t) => ({ nome: t.name, descricao: String(t.description || '').split(/\n|\. /)[0].slice(0, 120), args: Object.keys((t.inputSchema && t.inputSchema.properties) || {}) }));
|
|
87
|
+
} catch (e) { out[nome] = { erro: e.message }; }
|
|
88
|
+
}));
|
|
89
|
+
try { fs.mkdirSync(CASA, { recursive: true }); fs.writeFileSync(CACHE, JSON.stringify({ quando: Date.now(), servidores: out }, null, 2)); } catch { /* sem cache */ }
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function chamar(servidor, tool, args = {}, pasta) {
|
|
94
|
+
const def = servidores(pasta)[servidor];
|
|
95
|
+
if (!def) throw new Error(`não há servidor MCP "${servidor}" — veja primocode mcp list`);
|
|
96
|
+
const c = await conectar(def);
|
|
97
|
+
try {
|
|
98
|
+
const r = await c.pedir('tools/call', { name: tool, arguments: args });
|
|
99
|
+
return r;
|
|
100
|
+
} finally { c.fechar(); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A linha do contexto: só se houver servidor configurado, a partir do cache. */
|
|
104
|
+
function dica(pasta) {
|
|
105
|
+
const lista = Object.keys(servidores(pasta));
|
|
106
|
+
if (!lista.length) return '';
|
|
107
|
+
const c = lerJson(CACHE);
|
|
108
|
+
const det = lista.map((n) => {
|
|
109
|
+
const tools = c && c.servidores && Array.isArray(c.servidores[n]) ? c.servidores[n].map((t) => t.nome).slice(0, 8).join(', ') : '';
|
|
110
|
+
return tools ? `${n} (${tools})` : n;
|
|
111
|
+
}).join(' · ');
|
|
112
|
+
return `[mcp] Servidores MCP disponíveis: ${det}. Use pelo run_shell: primocode mcp call <servidor> <tool> --json '{...}'`
|
|
113
|
+
+ (c ? '' : ' (rode primocode mcp list para ver as tools).');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `primocode mcp …` */
|
|
117
|
+
async function main(argv) {
|
|
118
|
+
const [op, ...resto] = argv;
|
|
119
|
+
const json = (() => { const i = resto.indexOf('--json'); try { return i >= 0 ? JSON.parse(resto[i + 1] || '{}') : {}; } catch { return null; } })();
|
|
120
|
+
try {
|
|
121
|
+
if (!op || op === 'help') {
|
|
122
|
+
console.log('primocode mcp list — servidores e tools (de ~/.primocode/mcp.json e .mcp.json)');
|
|
123
|
+
console.log("primocode mcp call <servidor> <tool> --json '{...}' — chama uma tool; sai o resultado em JSON");
|
|
124
|
+
console.log('primocode mcp add <nome> -- <comando> [args…] — guarda um servidor em ~/.primocode/mcp.json');
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
if (op === 'list') { console.log(JSON.stringify(await listar(process.cwd()))); return 0; }
|
|
128
|
+
if (op === 'call') {
|
|
129
|
+
if (json === null) { console.log(JSON.stringify({ ok: false, error: '--json inválido' })); return 1; }
|
|
130
|
+
const r = await chamar(resto[0], resto[1], json, process.cwd());
|
|
131
|
+
const texto = (r.content || []).map((x) => (x.type === 'text' ? x.text : `[${x.type}]`)).join('\n');
|
|
132
|
+
console.log(JSON.stringify({ ok: !r.isError, resultado: texto.slice(0, 20000) }));
|
|
133
|
+
return r.isError ? 1 : 0;
|
|
134
|
+
}
|
|
135
|
+
if (op === 'add') {
|
|
136
|
+
const i = resto.indexOf('--');
|
|
137
|
+
const nome = resto[0];
|
|
138
|
+
const cmd = i >= 0 ? resto.slice(i + 1) : resto.slice(1);
|
|
139
|
+
if (!nome || !cmd.length) { console.log('uso: primocode mcp add <nome> -- <comando> [args…]'); return 1; }
|
|
140
|
+
const j = lerJson(CONFIG) || { mcpServers: {} };
|
|
141
|
+
j.mcpServers = j.mcpServers || {};
|
|
142
|
+
j.mcpServers[nome] = { command: cmd[0], args: cmd.slice(1) };
|
|
143
|
+
fs.mkdirSync(CASA, { recursive: true });
|
|
144
|
+
fs.writeFileSync(CONFIG, JSON.stringify(j, null, 2));
|
|
145
|
+
console.log(JSON.stringify({ ok: true, servidor: nome }));
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
console.log(JSON.stringify({ ok: false, error: `operação desconhecida: ${op}` }));
|
|
149
|
+
return 1;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.log(JSON.stringify({ ok: false, error: e.message }));
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { servidores, conectar, listar, chamar, dica, main };
|
package/lib/personas.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* personas.js — a equipe que trabalha calada por trás de cada pedido.
|
|
3
|
+
*
|
|
4
|
+
* "Coloca personas que fiquem trabalhando ocultamente, e que seja surreal."
|
|
5
|
+
* (30/09)
|
|
6
|
+
*
|
|
7
|
+
* Não são subagentes: cada subagente é mais uma ida ao modelo, e o plano
|
|
8
|
+
* grátis conta idas. A persona é o OLHAR de um especialista entrando no
|
|
9
|
+
* contexto do mesmo pedido — curta, e só a que o pedido pede. O agente faz o
|
|
10
|
+
* trabalho com o padrão de quem faria aquilo de profissão.
|
|
11
|
+
*
|
|
12
|
+
* escolher(pedido) → [{ nome, olhar }] no máximo duas
|
|
13
|
+
* bloco(pedido) → o texto que entra no contexto, ou ''
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const EQUIPE = [
|
|
19
|
+
{
|
|
20
|
+
nome: 'engenheira sênior', curto: 'código pequeno e certo, siga o padrão da casa, rode o que prova', quando: /\b(c[oó]digo|api|bug|erro|teste|fun[cç][aã]o|refator|backend|servidor|banco|typescript|javascript|python|node|react|next|deploy|build)\b/i,
|
|
21
|
+
olhar: 'Antes de escrever, olhe o que já existe e siga o padrão da casa. Mudança pequena e certa vence reescrita. '
|
|
22
|
+
+ 'Trate o erro de verdade (entrada vazia, rede caída), sem try/catch que engole tudo. Rode o teste ou o comando que prova. '
|
|
23
|
+
+ 'Biblioteca que você não domina: consulte a doc atual com primocode mcp call context7 query-docs antes de chutar a API.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
nome: 'designer de produto', curto: 'identidade própria, um acento, hierarquia, funciona em 390px', quando: /\b(site|p[aá]gina|landing|app|tela|interface|layout|dashboard|formul[aá]rio|loja)\b/i,
|
|
27
|
+
olhar: 'Nada de cara de template: uma tipografia com caráter (não Inter/Roboto por padrão), uma paleta tirada do assunto com UM acento, '
|
|
28
|
+
+ 'hierarquia clara, espaço para respirar, estados de foco e de vazio, e funciona em 390px. Texto real do negócio, nunca lorem ipsum.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
nome: 'diretor de motion', curto: 'o áudio comanda o quadro, uma ação por vez, respiro', quando: /\b(v[ií]deo|motion|anima[cç][aã]o|reels?|an[uú]ncio|vinheta|trailer)\b/i,
|
|
32
|
+
olhar: 'O áudio comanda o quadro: cada elemento entra na palavra que o pede. Uma ação dominante por vez, um acento só, '
|
|
33
|
+
+ 'respiro de 30–40% do quadro. Roteiro curto e falado, com uma palavra-tese por parte.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
nome: 'redatora', curto: 'frase curta, verbo forte, uma ideia por frase', quando: /\b(texto|copy|legenda|post|e-?mail|roteiro|t[ií]tulo|descri[cç][aã]o|artigo)\b/i,
|
|
37
|
+
olhar: 'Frase curta, verbo forte, uma ideia por frase. Fale com quem vai ler, no português de gente. Corte adjetivo que não carrega fato.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
/* Só quando a pessoa pede para MEXER no computador ("abre o YouTube",
|
|
41
|
+
"clica em…"). "Pesquisa sobre X" não é isso: era o que abria o Chrome
|
|
42
|
+
do nada, mostrava o mouse e parava (01/10). */
|
|
43
|
+
nome: 'operador', curto: 'só quando pedirem para mexer no computador; termine o que pediram', quando: /\b(abr[ae]|abrir|clica|clique|digita|navega)\b[\s\S]{0,40}\b(chrome|youtube|safari|firefox|spotify|whatsapp|navegador|site|aba|app|aplicativo|janela)\b|\b(mouse|teclado)\b/i,
|
|
44
|
+
olhar: 'A pessoa pediu para mexer no computador: faça à vista (browser_open, browser_read, clique pelas refs @e3) e TERMINE o que ela pediu — '
|
|
45
|
+
+ 'abrir a janela não é a tarefa. Diga em uma linha o que fez.',
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function escolher(pedido) {
|
|
50
|
+
const t = String(pedido || '');
|
|
51
|
+
return EQUIPE.filter((p) => p.quando.test(t)).slice(0, 2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* A IA ESCOLHE (01/10). Antes entravam só as que a regex casava — e a regex
|
|
55
|
+
errava para os dois lados (a "pesquisa" que chamava o operador). Agora a
|
|
56
|
+
equipe inteira vai, curta, e o modelo adota quem a tarefa pede; as que a
|
|
57
|
+
regex casou vêm primeiro, com o olhar inteiro, como sugestão — não ordem. */
|
|
58
|
+
function bloco(pedido) {
|
|
59
|
+
const t = String(pedido || '').trim();
|
|
60
|
+
if (!t || /^(oi|ol[aá]|e a[ií]|bom dia|boa tarde|boa noite|valeu|obrigad\w*|tudo bem)\b[\s\S]{0,20}$/i.test(t)) return '';
|
|
61
|
+
const sugeridas = escolher(t);
|
|
62
|
+
const linhas = sugeridas.map((p) => `- ${p.nome} (provável): ${p.olhar}`);
|
|
63
|
+
for (const p of EQUIPE) if (!sugeridas.includes(p)) linhas.push(`- ${p.nome}: ${p.curto}`);
|
|
64
|
+
return '[equipe] Adote o olhar de quem esta tarefa pede (no máximo dois; nenhum, se for conversa):\n' + linhas.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { escolher, bloco, EQUIPE };
|
package/lib/skills.js
CHANGED
|
@@ -408,7 +408,43 @@ function montar(texto, op = {}) {
|
|
|
408
408
|
return { texto: cabeca + texto, skills: escolhidas };
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
+
/**
|
|
412
|
+
* O CARDÁPIO: a IA escolhe (01/10). "Tem que ser a IA que vai decidir o que
|
|
413
|
+
* ela vai fazer." Antes a skill entrava INTEIRA por contagem de palavra — e
|
|
414
|
+
* "faz um roteiro de vídeo" trazia "Motion de primeira", que mandava animar.
|
|
415
|
+
* Agora entra uma linha por skill (as que casam com o pedido primeiro, até
|
|
416
|
+
* 12), e o modelo carrega a que servir com `primocode skill <nome>` — como a
|
|
417
|
+
* ferramenta Skill do Claude Code. Custa uma ida a mais só quando a skill é
|
|
418
|
+
* usada, e as linhas custam menos que um corpo inteiro.
|
|
419
|
+
*/
|
|
420
|
+
function cardapio(texto, { todas = null, max = 12 } = {}) {
|
|
421
|
+
const lista = (todas || listar()).filter((s) => s.ligada && s.corpo);
|
|
422
|
+
if (!lista.length) return { texto, sugeridas: [] };
|
|
423
|
+
const comNota = lista.map((s) => ({ s, pontos: pontuar(s, texto) }))
|
|
424
|
+
.sort((a, b) => b.pontos - a.pontos || a.s.nome.localeCompare(b.s.nome));
|
|
425
|
+
const linhas = comNota.slice(0, max).map(({ s, pontos }) =>
|
|
426
|
+
`- ${s.nome}${pontos ? ' (pode servir)' : ''} — ${String(s.descricao || s.titulo).replace(/\s+/g, ' ').slice(0, 90)}`);
|
|
427
|
+
const cabeca = '[skills] Você decide se usa alguma. Se uma é do tipo desta tarefa, CARREGUE antes de começar '
|
|
428
|
+
+ '(run_shell: primocode skill <nome>; várias na mesma resposta). Se nenhuma serve, ignore.\n'
|
|
429
|
+
+ linhas.join('\n') + '\n\nPEDIDO:\n';
|
|
430
|
+
return { texto: cabeca + texto, sugeridas: comNota.filter((x) => x.pontos > 0).map((x) => x.s) };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** `primocode skill <nome>` | `primocode skill list` — o corpo da skill, para o agente carregar. */
|
|
434
|
+
function main(argv) {
|
|
435
|
+
const [nome] = argv;
|
|
436
|
+
if (!nome || nome === 'list' || nome === 'help') {
|
|
437
|
+
for (const s of listar().filter((x) => x.ligada)) console.log(`${s.nome} — ${s.descricao || s.titulo}`);
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
const s = ler(apelidar(nome)) || listar().find((x) => x.nome.startsWith(String(nome).toLowerCase()));
|
|
441
|
+
if (!s) { console.log(`não há skill "${nome}" — veja primocode skill list`); return 1; }
|
|
442
|
+
console.log(`# ${s.titulo}\n\n${s.corpo}`);
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
|
|
411
446
|
module.exports = {
|
|
447
|
+
cardapio, main,
|
|
412
448
|
impressao,
|
|
413
449
|
CASA, ORIGEM, MAX_CARACTERES,
|
|
414
450
|
separar, listar, ler, criar, ligar, apagar, instalarDe, apelidar,
|
package/lib/tools.js
CHANGED
|
@@ -402,6 +402,13 @@ function readFile({ path: rawPath, offset, limit }, projectDir) {
|
|
|
402
402
|
};
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
/* Arquivo pequeno vem INTEIRO, mesmo pedido aos pedaços: o modelo lia um
|
|
406
|
+
app.js de 393 linhas de 50 em 50, cada pedaço uma ida ao servidor
|
|
407
|
+
(teste da loja, 01/10). Inteiro, uma vez, custa menos. */
|
|
408
|
+
if (todas.length <= 400 && bruto.length <= 40000) {
|
|
409
|
+
return { ok: true, content: todas.map((l, i) => `${i + 1}\t${l}`).join('\n'), linhas: todas.length, bytes: bruto.length,
|
|
410
|
+
inteiro: true, dica: 'arquivo pequeno: veio inteiro, com o número das linhas — não precisa ler de novo' };
|
|
411
|
+
}
|
|
405
412
|
const inicio = Math.max(0, (offset || 1) - 1);
|
|
406
413
|
const fim = Math.min(todas.length, inicio + (limit || 200));
|
|
407
414
|
const recorte = todas.slice(inicio, fim).map((l, i) => `${inicio + i + 1}\t${l}`).join('\n');
|
|
@@ -560,6 +567,14 @@ async function despachar(name, args, ctx) {
|
|
|
560
567
|
const alvo = /^https?:\/\//i.test(args.target || '')
|
|
561
568
|
? args.target
|
|
562
569
|
: resolveInProject(args.target, projectDir);
|
|
570
|
+
/* Site da web abre À VISTA no Chrome controlado (o cursor vai na
|
|
571
|
+
barra e digita). Arquivo do projeto continua no navegador padrão. */
|
|
572
|
+
if (/^https?:/i.test(alvo) && process.env.PRIMOCODE_BROWSER_HEADLESS !== '1' && require('./cursor').disponivel()) {
|
|
573
|
+
try {
|
|
574
|
+
const r = await browser.abrirVisivel(alvo);
|
|
575
|
+
if (r && r.ok !== false) return { ok: true, opener: 'chrome à vista', url: r.url, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR };
|
|
576
|
+
} catch { /* cai no navegador padrão */ }
|
|
577
|
+
}
|
|
563
578
|
return openInBrowser(alvo);
|
|
564
579
|
}
|
|
565
580
|
case 'multi_edit':
|
|
@@ -570,11 +585,14 @@ async function despachar(name, args, ctx) {
|
|
|
570
585
|
// Pesquisa de referência: abre documentação e exemplos sobre o
|
|
571
586
|
// pedido e devolve o TEXTO para o modelo aprender antes de
|
|
572
587
|
// escrever. Aparece no terminal, com as fontes — nada oculto.
|
|
573
|
-
case 'pesquisar_referencia':
|
|
574
|
-
|
|
588
|
+
case 'pesquisar_referencia': {
|
|
589
|
+
const r = await referencia.pesquisar({
|
|
575
590
|
consulta: args.consulta || args.query || '',
|
|
576
591
|
paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
|
|
577
592
|
});
|
|
593
|
+
// Escopo fechado ("só do site x.com"): o buscador devolve o mundo; aqui só passa o site.
|
|
594
|
+
return require('./estudio/referencias.js').filtrar(r);
|
|
595
|
+
}
|
|
578
596
|
|
|
579
597
|
// memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
|
|
580
598
|
// não alcançam por design (resolveInProject bloqueia ../ de propósito).
|
|
@@ -595,7 +613,8 @@ async function despachar(name, args, ctx) {
|
|
|
595
613
|
const alvo = /^https?:\/\//i.test(args.url || '')
|
|
596
614
|
? args.url
|
|
597
615
|
: 'file://' + resolveInProject(args.url, projectDir);
|
|
598
|
-
|
|
616
|
+
// Endereço da web abre À VISTA: o cursor vai na barra e digita (browser.abrirVisivel).
|
|
617
|
+
const r = /^https?:/i.test(alvo) ? await browser.abrirVisivel(alvo) : await browser.open(alvo);
|
|
599
618
|
return r && r.ok !== false ? { ...r, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR } : r;
|
|
600
619
|
}
|
|
601
620
|
case 'browser_click': {
|
package/lib/ui.js
CHANGED
|
@@ -310,11 +310,11 @@ const TIPS = [
|
|
|
310
310
|
];
|
|
311
311
|
|
|
312
312
|
const NEWS = [
|
|
313
|
-
'
|
|
314
|
-
'
|
|
315
|
-
'primocode video
|
|
316
|
-
'
|
|
317
|
-
'
|
|
313
|
+
'A IA decide o caminho: skills e equipe num cardápio, ela escolhe o que usa',
|
|
314
|
+
'Até 8 agentes em paralelo, cada um volta com uma entrega — e passa a vez',
|
|
315
|
+
'Edite falando no vídeo: primocode video auto aplica o que você diz, na hora',
|
|
316
|
+
'Colou texto grande? Vira etiqueta [Colado #1 · N linhas] na caixa',
|
|
317
|
+
'Escopo fechado de verdade: "só do site x.com" filtra a pesquisa',
|
|
318
318
|
];
|
|
319
319
|
|
|
320
320
|
// ── Cabeçalho ────────────────────────────────────────────────────────────
|
|
@@ -419,6 +419,21 @@ class Spinner {
|
|
|
419
419
|
// Fora de um terminal (pipe, CI) a animação só sujaria a saída.
|
|
420
420
|
if (!process.stdout.isTTY) return this;
|
|
421
421
|
this.desde = Date.now();
|
|
422
|
+
/* Com a caixa de digitar no ar, o spinner desenha DENTRO dela (ver
|
|
423
|
+
mostrarSpinner em lib/entrada.js): escrever "\r" por cima apagava
|
|
424
|
+
a caixa e a barra de status enquanto ele girava. */
|
|
425
|
+
const caixa = Spinner.caixa;
|
|
426
|
+
if (caixa && caixa.mostrarSpinner && caixa.desenhado !== undefined) {
|
|
427
|
+
this.naCaixa = caixa;
|
|
428
|
+
this.timer = setInterval(() => {
|
|
429
|
+
if (this.textos.length > 1) this.text = this.textos[Math.floor((Date.now() - this.desde) / 4000) % this.textos.length];
|
|
430
|
+
const seg = Math.floor((Date.now() - this.desde) / 1000);
|
|
431
|
+
const extra = (seg >= 3 ? ` · ${seg}s` : '') + (this.detalhe ? ` · ${this.detalhe}` : '');
|
|
432
|
+
caixa.mostrarSpinner(`${c.muted(this.frames[this.i])} ${c.muted(this.text)}${c.dim(extra)}`);
|
|
433
|
+
this.i = (this.i + 1) % this.frames.length;
|
|
434
|
+
}, 120);
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
422
437
|
this.timer = setInterval(() => {
|
|
423
438
|
// Com mais de uma frase, troca a cada 4s. A frase nova pode ser mais
|
|
424
439
|
// curta que a anterior: o resto da linha velha ficaria na tela, então
|
|
@@ -439,10 +454,29 @@ class Spinner {
|
|
|
439
454
|
return this;
|
|
440
455
|
}
|
|
441
456
|
stop() {
|
|
457
|
+
if (this.naCaixa) {
|
|
458
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
459
|
+
this.naCaixa.mostrarSpinner('');
|
|
460
|
+
this.naCaixa = null;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
442
463
|
if (this.timer) { clearInterval(this.timer); this.timer = null; process.stdout.write(`\r${' '.repeat(Math.max(80, this.largura + 4))}\r`); }
|
|
443
464
|
}
|
|
444
465
|
}
|
|
445
466
|
|
|
467
|
+
/* O TÍTULO DO TERMINAL: é por ele que o Orca (e qualquer terminal com abas)
|
|
468
|
+
sabe que este painel é um agente e se está trabalhando. O Orca lê o glifo
|
|
469
|
+
de spinner em braile como "trabalhando" e o nome como identidade — então
|
|
470
|
+
"⠋ PrimoCode — tarefa" enquanto trabalha, "PrimoCode" parado. Vai pelo
|
|
471
|
+
canal cru da caixa: pelo stdout embrulhado, um escape sem quebra de linha
|
|
472
|
+
deixaria a caixa apagada (o mesmo defeito do spinner). */
|
|
473
|
+
function titulo(texto) {
|
|
474
|
+
if (!process.stdout.isTTY || process.env.PRIMOCODE_SEM_TITULO === '1') return;
|
|
475
|
+
const limpo = String(texto || '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 80);
|
|
476
|
+
const escrever = Spinner.caixa && Spinner.caixa.escreverCru ? Spinner.caixa.escreverCru : (s) => process.stdout.write(s);
|
|
477
|
+
try { escrever(`\x1b]0;${limpo}\x07`); } catch { /* terminal fechado */ }
|
|
478
|
+
}
|
|
479
|
+
|
|
446
480
|
// O rótulo de cada tool na tela — o que a pessoa lê no lugar do nome interno.
|
|
447
481
|
const ROTULOS = {
|
|
448
482
|
read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
|
|
@@ -616,4 +650,5 @@ function friendlyError(raw) {
|
|
|
616
650
|
}
|
|
617
651
|
|
|
618
652
|
|
|
619
|
-
module.exports = {
|
|
653
|
+
module.exports = {
|
|
654
|
+
titulo, c, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
|