primocode 9.10.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 +30 -6
- package/bin/primocode.js +169 -342
- package/lib/act.js +360 -39
- package/lib/browser.js +105 -4
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +77 -11
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +21 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +66 -0
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +284 -53
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +13 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +78 -0
- package/lib/estudio/lancamento.js +441 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +139 -51
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +414 -0
- package/lib/fala.js +1 -94
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +44 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +100 -22
- package/lib/ui.js +50 -8
- package/lib/video/analise.js +89 -0
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +280 -0
- 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 +347 -0
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +274 -0
- package/lib/video/texto.js +144 -0
- package/lib/video/timeline.js +318 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +5 -5
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
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/pipeline.js
CHANGED
|
@@ -217,8 +217,8 @@ Duas regras que valem mais que o resto:
|
|
|
217
217
|
|
|
218
218
|
Responda com a especificação e mais nada.`;
|
|
219
219
|
|
|
220
|
-
const CONSTRUTOR_1 = (spec, pasta) => `Você é o CONSTRUTOR
|
|
221
|
-
o
|
|
220
|
+
const CONSTRUTOR_1 = (spec, pasta) => `Você é o CONSTRUTOR, e o seu trabalho é
|
|
221
|
+
o PRODUTO que FUNCIONA — de primeira, sem protótipo para alguém refazer depois.
|
|
222
222
|
|
|
223
223
|
Construa na pasta: ${pasta}
|
|
224
224
|
|
|
@@ -243,39 +243,35 @@ Regras:
|
|
|
243
243
|
digitar não mostra nada.
|
|
244
244
|
· Não deixe função pela metade nem "TODO". O que não der para fazer, não
|
|
245
245
|
prometa na tela.
|
|
246
|
+
· Com TELA: hierarquia de tamanhos, espaço para respirar, estados de foco e de
|
|
247
|
+
toque, mensagem que diz o que fazer quando o campo está vazio, e nada de
|
|
248
|
+
rolagem horizontal em 390px. Uma paleta, mantida.
|
|
249
|
+
· RÁPIDO: escreva cada arquivo UMA vez, inteiro, e vários na mesma rodada. O
|
|
250
|
+
resultado do write_file é a verdade — não releia o que acabou de escrever.
|
|
246
251
|
|
|
247
252
|
Escreva os arquivos e diga em uma linha o que ficou pronto.`;
|
|
248
253
|
|
|
249
|
-
const CONSTRUTOR_2 = (spec, pasta, relato) => `Você é o
|
|
250
|
-
|
|
254
|
+
const CONSTRUTOR_2 = (spec, pasta, relato) => `Você é o CORRETOR. O produto já
|
|
255
|
+
está construído na pasta ${pasta}, e alguém ABRIU e achou problema.
|
|
251
256
|
|
|
252
|
-
|
|
253
|
-
LEIA os arquivos que já estão lá antes de mexer. Não recomece do zero: o que
|
|
254
|
-
está funcionando fica.
|
|
255
|
-
|
|
256
|
-
Especificação:
|
|
257
|
+
O que foi pedido (resumo):
|
|
257
258
|
|
|
258
259
|
"""
|
|
259
260
|
${spec}
|
|
260
261
|
"""
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
O que você faz:
|
|
263
|
+
O que está errado:
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
· REDESENHA: hierarquia de tamanhos, espaço para respirar, estados de foco e de
|
|
269
|
-
toque, e o que aparece quando não há dados. Escolha uma paleta e mantenha.
|
|
270
|
-
· CUIDA DO ERRO: campo vazio, valor impossível, lista vazia. Cada um com uma
|
|
271
|
-
mensagem que diz o que fazer, não "erro".
|
|
272
|
-
· RESPONSIVO de verdade: nada de rolagem horizontal em 390px de largura.
|
|
265
|
+
"""
|
|
266
|
+
${relato || '(sem relato)'}
|
|
267
|
+
"""
|
|
273
268
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
269
|
+
· Conserte SÓ o que está apontado. Nada de recurso novo, nada de recomeçar.
|
|
270
|
+
· Leia apenas o arquivo citado no erro — não a pasta inteira — e troque o trecho
|
|
271
|
+
com edit_file. Reescrever o arquivo inteiro só se ele estiver vazio ou em
|
|
272
|
+
branco.
|
|
273
|
+
· Não releia o arquivo depois de editar: o resultado do edit_file é a verdade.
|
|
277
274
|
|
|
278
|
-
Não escreva "pronto" sem ter aberto os arquivos e mudado o que precisa.
|
|
279
275
|
Diga em uma linha o que mudou.`;
|
|
280
276
|
|
|
281
277
|
const REVISOR = (spec, pasta, arquivos, relato) => `Você é o REVISADOR, e você
|
|
@@ -327,11 +323,12 @@ const APROVOU = /^\s*APROVAD[OA]\b/im;
|
|
|
327
323
|
* @param {(prompt:string,rotulo:string)=>Promise<string>} op.correr roda um agente
|
|
328
324
|
* @param {(linha:string)=>void} op.mostrar escreve na tela do usuário
|
|
329
325
|
* @param {(alvo:string)=>any} [op.abrir] abre no navegador da pessoa
|
|
330
|
-
* @param {number} [op.rodadas]
|
|
326
|
+
* @param {number} [op.rodadas] conferências, contando a primeira (padrão 3)
|
|
331
327
|
*/
|
|
332
328
|
async function construir(op) {
|
|
333
329
|
const { pedido, pasta, correr, mostrar = () => {}, abrir } = op;
|
|
334
|
-
|
|
330
|
+
// Quantas CONFERÊNCIAS (a primeira + os consertos). 3 = até dois consertos.
|
|
331
|
+
const rodadasMax = Math.max(1, Math.min(4, op.rodadas == null ? 3 : op.rodadas));
|
|
335
332
|
if (!pedido) return { ok: false, error: 'Diga o que construir.' };
|
|
336
333
|
if (!pasta) return { ok: false, error: 'Diga em que pasta construir.' };
|
|
337
334
|
fs.mkdirSync(pasta, { recursive: true });
|
|
@@ -345,45 +342,47 @@ async function construir(op) {
|
|
|
345
342
|
return { ok: false, error: 'O engenheiro de prompt não devolveu especificação. Faça você mesmo.' };
|
|
346
343
|
}
|
|
347
344
|
|
|
348
|
-
mostrar('construtor
|
|
349
|
-
await correr(CONSTRUTOR_1(spec, pasta), 'construtor
|
|
350
|
-
etapas.push({ papel: 'construtor
|
|
351
|
-
|
|
345
|
+
mostrar('construtor');
|
|
346
|
+
await correr(CONSTRUTOR_1(spec, pasta), 'construtor');
|
|
347
|
+
etapas.push({ papel: 'construtor', arquivos: listar(pasta).length });
|
|
348
|
+
|
|
349
|
+
/* ── CONFERIR: a máquina primeiro, o modelo só quando ela não sabe ──
|
|
350
|
+
* Antes eram quatro agentes no mínimo: engenheiro, protótipo, um
|
|
351
|
+
* construtor 2 que RELIA tudo para refazer como MVP, e um revisor que
|
|
352
|
+
* relia de novo para opinar. Era o "escreve dois arquivos e fica o dia
|
|
353
|
+
* lendo o que fez". Agora: a página abriu limpa no Chrome escondido?
|
|
354
|
+
* Está pronta — a medida é fato, e opinião por cima dela só custava.
|
|
355
|
+
* Abriu com erro? O corretor recebe o erro e troca o trecho. O revisor
|
|
356
|
+
* de modelo só entra quando não há página para medir (um app de
|
|
357
|
+
* terminal), porque aí "não sei" precisa de alguém que leia. */
|
|
352
358
|
let relato = null;
|
|
353
359
|
let aprovado = false;
|
|
354
360
|
let ultimoRevisor = '';
|
|
355
361
|
|
|
356
362
|
for (let volta = 1; volta <= rodadasMax; volta++) {
|
|
357
|
-
mostrar(
|
|
358
|
-
await correr(CONSTRUTOR_2(spec, pasta, relato), `construtor 2 (${volta})`);
|
|
359
|
-
|
|
360
|
-
mostrar('revisador — testando escondido');
|
|
363
|
+
mostrar(volta === 1 ? 'conferindo — abrindo a página escondido' : `conferindo o conserto ${volta - 1}`);
|
|
361
364
|
const medida = await medirNoNavegador(pasta);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
porque o projeto não é uma página, ou porque não há navegador
|
|
377
|
-
nesta máquina — o que a máquina tem a dizer é "não sei", e "não
|
|
378
|
-
sei" não reprova. Sem esta distinção, um app de linha de comando
|
|
379
|
-
nunca seria aprovado: gastaria as voltas todas e terminaria
|
|
380
|
-
dizendo que não está pronto, sobre um trabalho que está. */
|
|
381
|
-
aprovado = APROVOU.test(ultimoRevisor || '') && !(medida.testou && !medida.limpo);
|
|
365
|
+
let veredito;
|
|
366
|
+
if (medida.testou) {
|
|
367
|
+
aprovado = medida.limpo;
|
|
368
|
+
veredito = medida.limpo ? 'APROVADO pela medida' : 'REPROVADO pela medida';
|
|
369
|
+
relato = medida.limpo ? null : medida.relato;
|
|
370
|
+
} else {
|
|
371
|
+
mostrar('revisador — lendo o que foi feito');
|
|
372
|
+
ultimoRevisor = await correr(
|
|
373
|
+
REVISOR(spec, pasta, listar(pasta), medida.relato), `revisador (${volta})`);
|
|
374
|
+
aprovado = APROVOU.test(ultimoRevisor || '');
|
|
375
|
+
veredito = (ultimoRevisor || '').split('\n')[0];
|
|
376
|
+
relato = aprovado ? null : ultimoRevisor;
|
|
377
|
+
}
|
|
378
|
+
etapas.push({ papel: `conferência ${volta}`, testou: medida.testou, limpo: medida.limpo, veredito });
|
|
382
379
|
if (aprovado) { mostrar('aprovado'); break; }
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
380
|
+
if (volta === rodadasMax) break;
|
|
381
|
+
if (!ultimoRevisor) ultimoRevisor = relato || '';
|
|
382
|
+
mostrar(`corretor — consertando (${volta})`);
|
|
383
|
+
await correr(CONSTRUTOR_2(spec, pasta, relato), `corretor (${volta})`);
|
|
386
384
|
}
|
|
385
|
+
if (!ultimoRevisor && relato) ultimoRevisor = relato;
|
|
387
386
|
|
|
388
387
|
const entrada = acharEntrada(pasta);
|
|
389
388
|
let abriu = false;
|
package/lib/repeticao.js
CHANGED
|
@@ -62,7 +62,9 @@ function palavras(texto) {
|
|
|
62
62
|
/** Quanto duas buscas se sobrepõem, de 0 a 1. */
|
|
63
63
|
function semelhanca(a, b) {
|
|
64
64
|
const A = palavras(a), B = palavras(b);
|
|
65
|
-
|
|
65
|
+
// Sem palavra que conte dos dois lados ("go" e "js"): só é a mesma busca
|
|
66
|
+
// se o texto for o mesmo.
|
|
67
|
+
if (!A.size || !B.size) return normalizar(a).trim() === normalizar(b).trim() ? 1 : 0;
|
|
66
68
|
let juntos = 0;
|
|
67
69
|
for (const p of A) if (B.has(p)) juntos++;
|
|
68
70
|
return juntos / (A.size + B.size - juntos);
|
package/lib/skills.js
CHANGED
|
@@ -80,7 +80,13 @@ function separar(texto) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function lerEstado() {
|
|
83
|
-
|
|
83
|
+
let e;
|
|
84
|
+
try { e = JSON.parse(fs.readFileSync(ESTADO, 'utf8')); } catch { e = null; }
|
|
85
|
+
// Um estado.json válido sem `desligadas` (um `{}`) derrubava /skills,
|
|
86
|
+
// /marketplace e a entrada das skills em todo pedido.
|
|
87
|
+
if (!e || typeof e !== 'object') e = {};
|
|
88
|
+
if (!Array.isArray(e.desligadas)) e.desligadas = [];
|
|
89
|
+
return e;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
function salvarEstado(estado) {
|
|
@@ -200,7 +206,7 @@ function ler(nome) {
|
|
|
200
206
|
quando: String(cabecalho.quando || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
|
201
207
|
autor: cabecalho.autor || '',
|
|
202
208
|
corpo,
|
|
203
|
-
ligada: !estado.desligadas.includes(nome),
|
|
209
|
+
ligada: !(Array.isArray(estado.desligadas) && estado.desligadas.includes(nome)),
|
|
204
210
|
doProduto: fs.existsSync(path.join(ORIGEM, nome, 'SKILL.md')),
|
|
205
211
|
caminho: arquivo,
|
|
206
212
|
};
|
|
@@ -402,7 +408,43 @@ function montar(texto, op = {}) {
|
|
|
402
408
|
return { texto: cabeca + texto, skills: escolhidas };
|
|
403
409
|
}
|
|
404
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
|
+
|
|
405
446
|
module.exports = {
|
|
447
|
+
cardapio, main,
|
|
406
448
|
impressao,
|
|
407
449
|
CASA, ORIGEM, MAX_CARACTERES,
|
|
408
450
|
separar, listar, ler, criar, ligar, apagar, instalarDe, apelidar,
|
package/lib/snapshot.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snapshot.js — a página como o agente precisa dela: o que dá para clicar e
|
|
3
|
+
* digitar, cada coisa com uma REF curta.
|
|
4
|
+
*
|
|
5
|
+
* @e1 [link] "Preços" → /precos
|
|
6
|
+
* @e2 [textbox] "E-mail" (seu@email.com)
|
|
7
|
+
* @e3 [button] "Entrar"
|
|
8
|
+
*
|
|
9
|
+
* Antes o agente lia o innerText da página (6.000 caracteres de texto corrido,
|
|
10
|
+
* sem dizer o que é botão) e chutava um seletor CSS — `input[name="q"]` no
|
|
11
|
+
* Google, que é `textarea`. Errava, lia de novo, chutava de novo. Com a ref,
|
|
12
|
+
* clicar é `browser_click selector:"@e3"`: sem CSS, sem chute. A ideia é a do
|
|
13
|
+
* agent-browser da Vercel e do Playwright MCP (árvore de acessibilidade com
|
|
14
|
+
* refs); o código é nosso, roda no mesmo CDP que o PrimoCode já usa e não pede
|
|
15
|
+
* instalar nada.
|
|
16
|
+
*
|
|
17
|
+
* A ref mora no próprio elemento (data-primo-ref). Navegou ou a página se
|
|
18
|
+
* redesenhou, a ref some — e o erro diz para ler de novo, em vez de clicar no
|
|
19
|
+
* lugar errado.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const ATRIBUTO = 'data-primo-ref';
|
|
25
|
+
|
|
26
|
+
/** O script que roda DENTRO da página e devolve { titulo, url, linhas, total }. */
|
|
27
|
+
function scriptDoRetrato(max = 80) {
|
|
28
|
+
return `(function(){
|
|
29
|
+
var MAX = ${Number(max) || 80};
|
|
30
|
+
var ATR = ${JSON.stringify(ATRIBUTO)};
|
|
31
|
+
var velhos = document.querySelectorAll('[' + ATR + ']');
|
|
32
|
+
for (var i = 0; i < velhos.length; i++) velhos[i].removeAttribute(ATR);
|
|
33
|
+
function visivel(el) {
|
|
34
|
+
var r = el.getBoundingClientRect();
|
|
35
|
+
if (r.width < 2 || r.height < 2) return false;
|
|
36
|
+
var s = getComputedStyle(el);
|
|
37
|
+
return s.visibility !== 'hidden' && s.display !== 'none' && Number(s.opacity) > 0.05;
|
|
38
|
+
}
|
|
39
|
+
function limpo(t, n) { return String(t || '').replace(/\\s+/g, ' ').trim().slice(0, n || 60); }
|
|
40
|
+
function papel(el) {
|
|
41
|
+
var r = el.getAttribute('role');
|
|
42
|
+
if (r) return r;
|
|
43
|
+
var tag = el.tagName.toLowerCase();
|
|
44
|
+
if (tag === 'a') return 'link';
|
|
45
|
+
if (tag === 'button' || tag === 'summary') return 'button';
|
|
46
|
+
if (tag === 'select') return 'select';
|
|
47
|
+
if (tag === 'textarea') return 'textbox';
|
|
48
|
+
if (tag === 'input') {
|
|
49
|
+
var t = (el.type || 'text').toLowerCase();
|
|
50
|
+
if (t === 'checkbox' || t === 'radio') return t;
|
|
51
|
+
if (t === 'submit' || t === 'button' || t === 'reset') return 'button';
|
|
52
|
+
return 'textbox';
|
|
53
|
+
}
|
|
54
|
+
if (el.isContentEditable) return 'textbox';
|
|
55
|
+
return 'button';
|
|
56
|
+
}
|
|
57
|
+
function nome(el) {
|
|
58
|
+
var n = el.getAttribute('aria-label') || el.getAttribute('title') || '';
|
|
59
|
+
if (!n && el.id) { var l = document.querySelector('label[for="' + el.id + '"]'); if (l) n = l.innerText; }
|
|
60
|
+
if (!n && el.closest && el.closest('label')) n = el.closest('label').innerText;
|
|
61
|
+
// O innerText de um select são TODAS as opções — o nome dele é o rótulo.
|
|
62
|
+
if (!n && el.tagName !== 'SELECT') n = el.innerText || el.value || el.getAttribute('alt') || '';
|
|
63
|
+
if (!n) { var img = el.querySelector && el.querySelector('img[alt]'); if (img) n = img.alt; }
|
|
64
|
+
return limpo(n);
|
|
65
|
+
}
|
|
66
|
+
var SEL = 'a[href],button,input:not([type=hidden]),textarea,select,summary,[contenteditable=""],[contenteditable=true],'
|
|
67
|
+
+ '[role=button],[role=link],[role=checkbox],[role=radio],[role=tab],[role=menuitem],[role=option],[role=textbox],'
|
|
68
|
+
+ '[role=combobox],[role=switch],[role=searchbox],[onclick],h1,h2,h3';
|
|
69
|
+
var linhas = [], n = 0, total = 0;
|
|
70
|
+
var todos = document.querySelectorAll(SEL);
|
|
71
|
+
for (var j = 0; j < todos.length; j++) {
|
|
72
|
+
var el = todos[j];
|
|
73
|
+
if (!visivel(el)) continue;
|
|
74
|
+
var tag = el.tagName.toLowerCase();
|
|
75
|
+
if (/^h[1-3]$/.test(tag)) { var h = limpo(el.innerText, 80); if (h) linhas.push('#'.repeat(Number(tag[1])) + ' ' + h); continue; }
|
|
76
|
+
total++;
|
|
77
|
+
if (n >= MAX) continue;
|
|
78
|
+
n++;
|
|
79
|
+
var ref = 'e' + n;
|
|
80
|
+
el.setAttribute(ATR, ref);
|
|
81
|
+
var p = papel(el), txt = nome(el), extra = '';
|
|
82
|
+
if (p === 'link') { var href = el.getAttribute('href') || ''; if (href && href.indexOf('javascript:') !== 0) extra = ' → ' + href.slice(0, 60); }
|
|
83
|
+
if (p === 'textbox' && el.placeholder && el.placeholder !== txt) extra = ' (' + limpo(el.placeholder, 40) + ')';
|
|
84
|
+
if ((p === 'textbox' || p === 'select') && el.value) extra += ' = "' + limpo(el.value, 40) + '"';
|
|
85
|
+
if ((p === 'checkbox' || p === 'radio') && el.checked) extra += ' [marcado]';
|
|
86
|
+
if (el.disabled) extra += ' [desativado]';
|
|
87
|
+
linhas.push('@' + ref + ' [' + p + '] "' + txt + '"' + extra);
|
|
88
|
+
}
|
|
89
|
+
return { titulo: document.title, url: location.href, linhas: linhas, total: total };
|
|
90
|
+
})()`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Texto do retrato, pronto para o modelo. */
|
|
94
|
+
function formatar(r, max = 80) {
|
|
95
|
+
if (!r) return '';
|
|
96
|
+
const cab = `${r.titulo || '(sem título)'} — ${r.url || ''}`;
|
|
97
|
+
const mais = r.total > max ? `\n… e mais ${r.total - max} elementos (role a página e leia de novo)` : '';
|
|
98
|
+
return `${cab}\n${(r.linhas || []).join('\n')}${mais}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** "@e3" ou "e3" → o seletor CSS da ref; qualquer outra coisa passa igual. */
|
|
102
|
+
function seletorDaRef(s) {
|
|
103
|
+
const m = /^@?(e\d+)$/.exec(String(s || '').trim());
|
|
104
|
+
return m ? `[${ATRIBUTO}="${m[1]}"]` : s;
|
|
105
|
+
}
|
|
106
|
+
function ehRef(s) { return /^@?e\d+$/.test(String(s || '').trim()); }
|
|
107
|
+
|
|
108
|
+
const COMO_USAR = 'Para agir, use a ref como selector: browser_click selector:"@e3", '
|
|
109
|
+
+ 'browser_type selector:"@e2" text:"…". A ref vale até a página mudar; depois de navegar, leia de novo.';
|
|
110
|
+
|
|
111
|
+
module.exports = { scriptDoRetrato, formatar, seletorDaRef, ehRef, COMO_USAR, ATRIBUTO };
|