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.
Files changed (89) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +169 -342
  3. package/lib/act.js +360 -39
  4. package/lib/browser.js +105 -4
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/claude-engine.js +38 -17
  8. package/lib/codex-engine.js +27 -9
  9. package/lib/config.js +10 -3
  10. package/lib/cursor.js +10 -1
  11. package/lib/entrada.js +77 -11
  12. package/lib/estudio/cinema.js +27 -9
  13. package/lib/estudio/diretor.js +21 -1
  14. package/lib/estudio/ende.js +16 -5
  15. package/lib/estudio/estilo.js +66 -0
  16. package/lib/estudio/gerativo.js +1453 -0
  17. package/lib/estudio/glifos.js +215 -0
  18. package/lib/estudio/hibrido.js +165 -0
  19. package/lib/estudio/index.js +284 -53
  20. package/lib/estudio/kit/base.jsx +285 -0
  21. package/lib/estudio/kit/gerativo.jsx +980 -0
  22. package/lib/estudio/kit/index.jsx +13 -0
  23. package/lib/estudio/kit/marca.jsx +741 -0
  24. package/lib/estudio/kit/tres.jsx +308 -0
  25. package/lib/estudio/kit/vox.jsx +357 -0
  26. package/lib/estudio/kit.js +78 -0
  27. package/lib/estudio/lancamento.js +441 -0
  28. package/lib/estudio/motion.js +2 -5
  29. package/lib/estudio/plano.js +28 -3
  30. package/lib/estudio/referencias.js +105 -0
  31. package/lib/estudio/remotion.js +139 -51
  32. package/lib/estudio/ritmo.js +77 -0
  33. package/lib/estudio/sons-leves.js +38 -1
  34. package/lib/estudio/variedade.js +105 -0
  35. package/lib/estudio/vox.js +414 -0
  36. package/lib/fala.js +1 -94
  37. package/lib/mcp.js +156 -0
  38. package/lib/personas.js +67 -0
  39. package/lib/pipeline.js +55 -56
  40. package/lib/repeticao.js +3 -1
  41. package/lib/skills.js +44 -2
  42. package/lib/snapshot.js +111 -0
  43. package/lib/tools.js +100 -22
  44. package/lib/ui.js +50 -8
  45. package/lib/video/analise.js +89 -0
  46. package/lib/video/auto.js +329 -0
  47. package/lib/video/camadas.js +207 -0
  48. package/lib/video/cli.js +74 -0
  49. package/lib/video/delta.js +90 -0
  50. package/lib/video/edits.js +379 -0
  51. package/lib/video/filters.js +280 -0
  52. package/lib/video/fx3d/Camadas.jsx +64 -0
  53. package/lib/video/fx3d/Objetos.jsx +128 -0
  54. package/lib/video/fx3d/Root.jsx +18 -0
  55. package/lib/video/fx3d/modelos.jsx +304 -0
  56. package/lib/video/fx3d.js +108 -0
  57. package/lib/video/gerados.js +119 -0
  58. package/lib/video/objetos.js +111 -0
  59. package/lib/video/ops-fx.js +453 -0
  60. package/lib/video/ops.js +347 -0
  61. package/lib/video/paralelo.js +50 -0
  62. package/lib/video/pedido.js +191 -0
  63. package/lib/video/projeto.js +133 -0
  64. package/lib/video/rastrear.js +95 -0
  65. package/lib/video/rastro.js +276 -0
  66. package/lib/video/render.js +274 -0
  67. package/lib/video/texto.js +144 -0
  68. package/lib/video/timeline.js +318 -0
  69. package/lib/video/transcript.js +124 -0
  70. package/lib/voz.js +75 -223
  71. package/package.json +2 -3
  72. package/skills/motion-de-primeira/SKILL.md +5 -5
  73. package/studio/narrar.py +10 -7
  74. package/studio/rastrear.py +399 -0
  75. package/studio/texto.py +208 -0
  76. package/voz/primo_voz/__main__.py +96 -236
  77. package/voz/primo_voz/transcrever.py +31 -1
  78. package/app/ansi.js +0 -55
  79. package/app/app.css +0 -500
  80. package/app/app.js +0 -1017
  81. package/app/icone.svg +0 -6
  82. package/app/index.html +0 -220
  83. package/app/primo-marca.js +0 -192
  84. package/lib/app.js +0 -615
  85. package/lib/icone.js +0 -263
  86. package/lib/instalar-app.js +0 -231
  87. package/lib/painel.js +0 -291
  88. package/voz/primo_voz/desperta.py +0 -195
  89. 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 };
@@ -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 1, e o seu trabalho é
221
- o PROTÓTIPO que FUNCIONA.
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 CONSTRUTOR 2, e o seu
250
- trabalho é virar o protótipo em PRODUTO.
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
- A pasta é: ${pasta}
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
- ${relato ? `O revisor testou o que existe e achou isto:\n\n"""\n${relato}\n"""\n\n` : ''}\
263
- O que você faz:
263
+ O que está errado:
264
264
 
265
- · CONSERTA o que o revisor apontou. Isso vem primeiro; nada de recurso novo com
266
- erro aberto.
267
- · COMPLETA o que a especificação pede e o protótipo não faz.
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
- As três últimas valem para o que TEM TELA. Se isto roda no terminal, o
275
- equivalente é: mensagem de uso quando faltar argumento, saída legível, e código
276
- de saída diferente de zero quando der errado.
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] voltas de correção (padrão 2)
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
- const rodadasMax = Math.max(1, Math.min(4, op.rodadas == null ? 2 : op.rodadas));
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 1 — protótipo');
349
- await correr(CONSTRUTOR_1(spec, pasta), 'construtor 1');
350
- etapas.push({ papel: 'construtor 1', arquivos: listar(pasta).length });
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(`construtor 2 — ${volta === 1 ? 'MVP' : `correção ${volta - 1}`}`);
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
- const arquivos = listar(pasta);
363
- ultimoRevisor = await correr(
364
- REVISOR(spec, pasta, arquivos, medida.relato), `revisador (${volta})`);
365
-
366
- etapas.push({ papel: `revisador ${volta}`, testou: medida.testou,
367
- limpo: medida.limpo, veredito: (ultimoRevisor || '').split('\n')[0] });
368
-
369
- /* O veredito do modelo NÃO passa por cima do que a máquina mediu.
370
- Página em branco ou erro de JavaScript é reprovação, e um
371
- "APROVADO" escrito em cima disso é o revisor lendo o que ele
372
- queria que estivesse lá — que é exatamente o defeito que separar
373
- os papéis existe para evitar.
374
-
375
- Mas o VETO precisa de MEDIDA: quando não deu para abrir nada —
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
- relato = medida.limpo ? ultimoRevisor : `${medida.relato}\n\n${ultimoRevisor || ''}`;
385
- if (volta < rodadasMax) mostrar('reprovado — voltando para o construtor');
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
- if (!A.size || !B.size) return A.size === B.size ? 1 : 0;
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
- try { return JSON.parse(fs.readFileSync(ESTADO, 'utf8')); } catch { return { desligadas: [] }; }
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,
@@ -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 };