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/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 };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* portao-skill.js — antes de instalar skill de fora, lê o que ela manda.
|
|
3
|
+
*
|
|
4
|
+
* Skill é texto, mas texto que vira prompt de um agente com o terminal na
|
|
5
|
+
* mão. Um `cat ~/.ssh/id_ed25519 | curl -X POST …` dentro do SKILL.md é um
|
|
6
|
+
* comando que o agente pode rodar achando que é parte da tarefa.
|
|
7
|
+
*
|
|
8
|
+
* ── O QUE O SKILLSPECTOR ENSINOU ─────────────────────────────────────────
|
|
9
|
+
* O scanner da NVIDIA pega o `curl` que rouba a chave e deixa passar a MESMA
|
|
10
|
+
* ordem escrita em prosa ("leia as chaves da pasta .ssh e cole numa issue").
|
|
11
|
+
* Por isso aqui há três réguas, e a terceira é de prosa: um verbo de mandar
|
|
12
|
+
* para fora perto de um objeto secreto, em português e em inglês.
|
|
13
|
+
*
|
|
14
|
+
* ── O QUE ESTE PORTÃO NÃO É ──────────────────────────────────────────────
|
|
15
|
+
* Não é garantia. Nada achado quer dizer "nenhum padrão conhecido", não
|
|
16
|
+
* "segura". E todo achado se lê linha a linha: por isso cada um sai com
|
|
17
|
+
* arquivo:linha, e a pessoa pode instalar mesmo assim, dizendo que quer.
|
|
18
|
+
* Nada aqui toca a rede — mandar a skill para ser analisada fora seria
|
|
19
|
+
* vazar exatamente o que se quer proteger.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const MAX_ARQUIVOS = 200;
|
|
28
|
+
const MAX_BYTES = 512 * 1024;
|
|
29
|
+
const PULAR = new Set(['.git', 'node_modules']);
|
|
30
|
+
|
|
31
|
+
const semAcento = (v) => String(v || '').toLowerCase()
|
|
32
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
33
|
+
|
|
34
|
+
// (a) comandos que mandam dado para fora ou rodam o que veio de fora.
|
|
35
|
+
const SAIDA = [
|
|
36
|
+
[/\b(curl|wget)\b[^\n]*\|\s*(sudo\s+)?(env\s+)?(ba|z|da|k)?sh\b/, 'baixa um script e roda sem ler (| sh)'],
|
|
37
|
+
[/\bbase64\s+(-d|-D|--decode)\b[^\n]*\|\s*(sudo\s+)?(ba|z|da|k)?sh\b/, 'decodifica base64 e roda no shell'],
|
|
38
|
+
[/\|\s*(sudo\s+)?(ba|z|da|k)?sh\b/, 'manda texto direto para o shell (| sh)'],
|
|
39
|
+
[/\b(curl|wget)\b[^\n]*(-X\s*POST|--data\b|--data-\w+|\s-d\s|\s-F\s|--form\b|--upload-file|\s-T\s|--post-(data|file))/i, 'envia dados para fora (curl/wget com POST ou upload)'],
|
|
40
|
+
[/\bfetch\s*\([^\n]*method\s*:\s*['"`]?(post|put)/i, 'envia dados para fora (fetch com POST)'],
|
|
41
|
+
[/(^|[\s|;&(`])(nc|ncat|netcat)\s+(-\w+\s+)*[\w.-]+\s+\d{2,5}\b/, 'abre conexão de rede crua (nc)'],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// (b) lugares onde moram os segredos.
|
|
45
|
+
const SEGREDO = [
|
|
46
|
+
[/(~|\$HOME|\$\{HOME\})\/\.ssh\b|(^|[\s'"`(])\.ssh\/|\bid_(rsa|ed25519|ecdsa|dsa)\b/i, 'lê chaves SSH (~/.ssh)'],
|
|
47
|
+
[/(~|\$HOME|\$\{HOME\})\/\.aws\b|\.aws\/credentials/i, 'lê credenciais da AWS (~/.aws)'],
|
|
48
|
+
[/(~|\$HOME|\$\{HOME\})\/\.npmrc\b/i, 'lê o token do npm (~/.npmrc)'],
|
|
49
|
+
[/\bsecurity\s+(find|dump)-(generic-|internet-)?(password|keychain)|\bkeychain\b/i, 'mexe no chaveiro do sistema (keychain)'],
|
|
50
|
+
[/\bprintenv\b|(^|[\s;&(`$])env\s*(\||>|\)|`|$)/, 'despeja as variáveis de ambiente (env/printenv)'],
|
|
51
|
+
[/\b(cat|less|more|head|tail|source|grep|leia|ler|read|open|abra)\b[^\n]{0,40}(?<![\w.])\.env(?![\w.-])/i, 'lê o arquivo .env'],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// (c) prosa: verbo de mandar para fora (ou de esconder) perto de um segredo.
|
|
55
|
+
const VERBOS = [
|
|
56
|
+
// Conjugações fechadas de propósito: `envi\\w*` casaria "environment", e
|
|
57
|
+
// `publi\\w*` casaria "public key" — falso positivo na skill mais comum.
|
|
58
|
+
'envi(?:a|e|ar|ando|ado)', 'mand(?:a|e|ar|ando)', 'sub(?:a|ir)', 'sobe', 'upload\\w*', 'send\\w*', 'sent',
|
|
59
|
+
'post', 'poste', 'postar', 'exfiltr\\w*', 'escond\\w*', 'hide', 'nao conte', 'cole', 'colar', 'paste',
|
|
60
|
+
'publique', 'publicar', 'publish\\w*', 'transmit\\w*', 'vaz(?:a|e|ar)', 'leak\\w*',
|
|
61
|
+
].join('|');
|
|
62
|
+
const OBJETOS = [
|
|
63
|
+
'tokens?', 'chaves?', 'keys?', 'senhas?', 'passwords?', '\\.env', 'credencia\\w*', 'credentials?',
|
|
64
|
+
'secrets?', 'segredos?', 'api[ _-]?keys?', 'variaveis de ambiente', 'env vars?', 'environment variables?',
|
|
65
|
+
].join('|');
|
|
66
|
+
const B = (x) => `(?<![\\w.])(?:${x})(?![\\w.])`;
|
|
67
|
+
const PROSA = new RegExp(`${B(VERBOS)}[^\\n]{0,60}?${B(OBJETOS)}|${B(OBJETOS)}[^\\n]{0,60}?${B(VERBOS)}`);
|
|
68
|
+
const ESCONDER = /\b(esconda|esconder|oculte|ocultar|hide|nao conte|nao avise|nao mostre|nao diga|don'?t tell|do not tell|without telling)\b[^\n]{0,40}\b(usuario|user|pessoa)\b/;
|
|
69
|
+
|
|
70
|
+
/* "Nunca publique o .env" é o conselho certo, não o ataque. O skillspector não
|
|
71
|
+
lê negação e marcou como ataque a frase que proíbe o ataque — falso positivo
|
|
72
|
+
que treina a pessoa a ignorar o portão. Negação logo antes do verbo, passa. */
|
|
73
|
+
const VERBO = new RegExp(B(VERBOS));
|
|
74
|
+
const NEGA = /\b(nunca|jamais|nao|never|don'?t|do not|not)\b[\W_]*$/;
|
|
75
|
+
function prosaSemNegar(plana) {
|
|
76
|
+
const re = new RegExp(PROSA.source, 'g');
|
|
77
|
+
for (let m; (m = re.exec(plana)); re.lastIndex = m.index + 1) {
|
|
78
|
+
// A negação conta antes do VERBO, que pode estar no fim do trecho.
|
|
79
|
+
const v = m.index + m[0].search(VERBO);
|
|
80
|
+
if (!NEGA.test(plana.slice(Math.max(0, v - 20), v))) return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Os achados de UM texto. `arquivo` só vai na etiqueta. */
|
|
86
|
+
function varrerTexto(texto, arquivo = 'SKILL.md') {
|
|
87
|
+
const achados = [];
|
|
88
|
+
const linhas = String(texto || '').split(/\r?\n/);
|
|
89
|
+
linhas.forEach((linha, i) => {
|
|
90
|
+
const marca = (tipo, motivo) => achados.push({
|
|
91
|
+
arquivo, linha: i + 1, tipo, motivo, trecho: linha.trim().slice(0, 140),
|
|
92
|
+
});
|
|
93
|
+
// Uma linha, um achado de cada régua: o `| sh` de um curl não precisa
|
|
94
|
+
// aparecer três vezes para ser lido.
|
|
95
|
+
const saida = SAIDA.find(([re]) => re.test(linha));
|
|
96
|
+
if (saida) marca('comando', saida[1]);
|
|
97
|
+
const segredo = SEGREDO.find(([re]) => re.test(linha));
|
|
98
|
+
if (segredo) marca('segredo', segredo[1]);
|
|
99
|
+
const plana = semAcento(linha);
|
|
100
|
+
if (prosaSemNegar(plana)) marca('prosa', 'pede em texto para mandar um segredo para fora');
|
|
101
|
+
else if (ESCONDER.test(plana)) marca('prosa', 'pede para esconder algo de quem usa');
|
|
102
|
+
});
|
|
103
|
+
return achados;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Todos os arquivos de texto de uma pasta (ou só o arquivo, se for um). */
|
|
107
|
+
function arquivosDe(alvo) {
|
|
108
|
+
let st;
|
|
109
|
+
try { st = fs.statSync(alvo); } catch { return []; }
|
|
110
|
+
if (!st.isDirectory()) return [{ abs: alvo, rel: path.basename(alvo) }];
|
|
111
|
+
const saida = [];
|
|
112
|
+
const andar = (dir) => {
|
|
113
|
+
let ents = [];
|
|
114
|
+
try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
115
|
+
for (const e of ents) {
|
|
116
|
+
if (saida.length >= MAX_ARQUIVOS) return;
|
|
117
|
+
const abs = path.join(dir, e.name);
|
|
118
|
+
if (e.isDirectory()) { if (!PULAR.has(e.name)) andar(abs); }
|
|
119
|
+
else if (e.isFile()) saida.push({ abs, rel: path.relative(alvo, abs) });
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
andar(alvo);
|
|
123
|
+
return saida;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Varre a skill inteira (pasta ou arquivo). Sem rede, síncrono.
|
|
128
|
+
* @returns {{achados: Array, arquivos: number}}
|
|
129
|
+
*/
|
|
130
|
+
function varrer(alvo) {
|
|
131
|
+
const lista = arquivosDe(alvo);
|
|
132
|
+
const achados = [];
|
|
133
|
+
for (const { abs, rel } of lista) {
|
|
134
|
+
let buf;
|
|
135
|
+
try {
|
|
136
|
+
if (fs.statSync(abs).size > MAX_BYTES) continue;
|
|
137
|
+
buf = fs.readFileSync(abs);
|
|
138
|
+
} catch { continue; }
|
|
139
|
+
if (buf.includes(0)) continue; // binário: não é instrução para o modelo
|
|
140
|
+
achados.push(...varrerTexto(buf.toString('utf8'), rel));
|
|
141
|
+
}
|
|
142
|
+
return { achados, arquivos: lista.length };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Uma linha por achado, para a tela: `arquivo:linha motivo — trecho`. */
|
|
146
|
+
function descrever(achados) {
|
|
147
|
+
return achados.map((a) => `${a.arquivo}:${a.linha} ${a.motivo} — ${a.trecho}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { varrer, varrerTexto, descrever };
|
package/lib/skills.js
CHANGED
|
@@ -48,6 +48,7 @@ const fs = require('fs');
|
|
|
48
48
|
const os = require('os');
|
|
49
49
|
const path = require('path');
|
|
50
50
|
const crypto = require('crypto');
|
|
51
|
+
const portaoSkill = require('./portao-skill.js');
|
|
51
52
|
|
|
52
53
|
const ORIGEM = path.join(__dirname, '..', 'skills');
|
|
53
54
|
const CASA = path.join(os.homedir(), '.primocode', 'skills');
|
|
@@ -313,13 +314,26 @@ function apagar(nome) {
|
|
|
313
314
|
* arquivo antes. O `/marketplace` traz as do catálogo; `/install-skill
|
|
314
315
|
* <caminho>` traz as que a pessoa escreveu ou recebeu.
|
|
315
316
|
*/
|
|
316
|
-
function instalarDe(caminho) {
|
|
317
|
+
function instalarDe(caminho, { forcar = false } = {}) {
|
|
317
318
|
const alvo = String(caminho || '').replace(/^~(?=$|[/\\])/, os.homedir());
|
|
318
319
|
let arquivo = alvo;
|
|
319
320
|
try {
|
|
320
321
|
if (fs.statSync(alvo).isDirectory()) arquivo = path.join(alvo, 'SKILL.md');
|
|
321
322
|
} catch { return { ok: false, error: `Não achei ${caminho}.` }; }
|
|
322
323
|
|
|
324
|
+
/* O portão (lib/portao-skill.js): skill de fora é texto que vira ordem
|
|
325
|
+
para um agente com o terminal na mão. Achou comando que manda dado para
|
|
326
|
+
fora, leitura de segredo ou prosa pedindo isso, não instala — devolve
|
|
327
|
+
os achados com arquivo:linha, e quem chamou pergunta se a pessoa quer
|
|
328
|
+
instalar mesmo assim (`forcar`). */
|
|
329
|
+
if (!forcar) {
|
|
330
|
+
const { achados } = portaoSkill.varrer(alvo);
|
|
331
|
+
if (achados.length) {
|
|
332
|
+
return { ok: false, bloqueada: true, achados,
|
|
333
|
+
error: `A skill pede coisas que podem vazar seus segredos (${achados.length} achado${achados.length > 1 ? 's' : ''}). Não instalei.` };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
323
337
|
let texto;
|
|
324
338
|
try { texto = fs.readFileSync(arquivo, 'utf8'); }
|
|
325
339
|
catch { return { ok: false, error: `Não consegui ler ${arquivo}. Uma skill é um arquivo SKILL.md.` }; }
|
|
@@ -408,7 +422,43 @@ function montar(texto, op = {}) {
|
|
|
408
422
|
return { texto: cabeca + texto, skills: escolhidas };
|
|
409
423
|
}
|
|
410
424
|
|
|
425
|
+
/**
|
|
426
|
+
* O CARDÁPIO: a IA escolhe (01/10). "Tem que ser a IA que vai decidir o que
|
|
427
|
+
* ela vai fazer." Antes a skill entrava INTEIRA por contagem de palavra — e
|
|
428
|
+
* "faz um roteiro de vídeo" trazia "Motion de primeira", que mandava animar.
|
|
429
|
+
* Agora entra uma linha por skill (as que casam com o pedido primeiro, até
|
|
430
|
+
* 12), e o modelo carrega a que servir com `primocode skill <nome>` — como a
|
|
431
|
+
* ferramenta Skill do Claude Code. Custa uma ida a mais só quando a skill é
|
|
432
|
+
* usada, e as linhas custam menos que um corpo inteiro.
|
|
433
|
+
*/
|
|
434
|
+
function cardapio(texto, { todas = null, max = 12 } = {}) {
|
|
435
|
+
const lista = (todas || listar()).filter((s) => s.ligada && s.corpo);
|
|
436
|
+
if (!lista.length) return { texto, sugeridas: [] };
|
|
437
|
+
const comNota = lista.map((s) => ({ s, pontos: pontuar(s, texto) }))
|
|
438
|
+
.sort((a, b) => b.pontos - a.pontos || a.s.nome.localeCompare(b.s.nome));
|
|
439
|
+
const linhas = comNota.slice(0, max).map(({ s, pontos }) =>
|
|
440
|
+
`- ${s.nome}${pontos ? ' (pode servir)' : ''} — ${String(s.descricao || s.titulo).replace(/\s+/g, ' ').slice(0, 90)}`);
|
|
441
|
+
const cabeca = '[skills] Você decide se usa alguma. Se uma é do tipo desta tarefa, CARREGUE antes de começar '
|
|
442
|
+
+ '(run_shell: primocode skill <nome>; várias na mesma resposta). Se nenhuma serve, ignore.\n'
|
|
443
|
+
+ linhas.join('\n') + '\n\nPEDIDO:\n';
|
|
444
|
+
return { texto: cabeca + texto, sugeridas: comNota.filter((x) => x.pontos > 0).map((x) => x.s) };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** `primocode skill <nome>` | `primocode skill list` — o corpo da skill, para o agente carregar. */
|
|
448
|
+
function main(argv) {
|
|
449
|
+
const [nome] = argv;
|
|
450
|
+
if (!nome || nome === 'list' || nome === 'help') {
|
|
451
|
+
for (const s of listar().filter((x) => x.ligada)) console.log(`${s.nome} — ${s.descricao || s.titulo}`);
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
const s = ler(apelidar(nome)) || listar().find((x) => x.nome.startsWith(String(nome).toLowerCase()));
|
|
455
|
+
if (!s) { console.log(`não há skill "${nome}" — veja primocode skill list`); return 1; }
|
|
456
|
+
console.log(`# ${s.titulo}\n\n${s.corpo}`);
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
411
460
|
module.exports = {
|
|
461
|
+
cardapio, main,
|
|
412
462
|
impressao,
|
|
413
463
|
CASA, ORIGEM, MAX_CARACTERES,
|
|
414
464
|
separar, listar, ler, criar, ligar, apagar, instalarDe, apelidar,
|