primocode 9.8.0-beta.4 → 9.8.0-beta.6

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 (52) hide show
  1. package/README.md +15 -4
  2. package/app/ansi.js +55 -0
  3. package/app/ide.css +345 -0
  4. package/app/ide.html +183 -0
  5. package/app/ide.js +639 -0
  6. package/bin/primocode.js +315 -83
  7. package/kits/acolhedor/index.html +130 -0
  8. package/kits/acolhedor/script.js +25 -0
  9. package/kits/acolhedor/style.css +146 -0
  10. package/kits/editorial/index.html +115 -0
  11. package/kits/editorial/script.js +19 -0
  12. package/kits/editorial/style.css +112 -0
  13. package/kits/premium/index.html +83 -0
  14. package/kits/premium/script.js +11 -0
  15. package/kits/premium/style.css +87 -0
  16. package/kits/vitrine/index.html +108 -0
  17. package/kits/vitrine/script.js +20 -0
  18. package/kits/vitrine/style.css +117 -0
  19. package/lib/app.js +41 -6
  20. package/lib/browser.js +14 -1
  21. package/lib/checkpoint-git.js +218 -0
  22. package/lib/checkpoint.js +159 -44
  23. package/lib/comandos-extra.js +481 -0
  24. package/lib/config.js +5 -2
  25. package/lib/ide.js +337 -0
  26. package/lib/kits.js +165 -54
  27. package/lib/skills.js +6 -1
  28. package/lib/ui.js +171 -17
  29. package/package.json +2 -2
  30. package/skills/apresentacao-pptx/SKILL.md +52 -0
  31. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  32. package/skills/cartaz-de-arte/SKILL.md +47 -0
  33. package/skills/criar-skill/LICENSE.txt +202 -0
  34. package/skills/criar-skill/SKILL.md +49 -0
  35. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  36. package/skills/depurar-de-verdade/SKILL.md +14 -5
  37. package/skills/documento-word/SKILL.md +57 -0
  38. package/skills/interface-revisada/SKILL.md +47 -0
  39. package/skills/me-ensina/LICENSE.txt +21 -0
  40. package/skills/me-ensina/SKILL.md +27 -0
  41. package/skills/me-entrevista/LICENSE.txt +21 -0
  42. package/skills/me-entrevista/SKILL.md +27 -0
  43. package/skills/pdf-bonito/SKILL.md +48 -0
  44. package/skills/planilha-excel/SKILL.md +47 -0
  45. package/skills/react-sem-dor/SKILL.md +48 -0
  46. package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
  47. package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
  48. package/skills/temas-prontos/LICENSE.txt +202 -0
  49. package/skills/temas-prontos/SKILL.md +50 -0
  50. package/skills/testar-o-site/LICENSE.txt +202 -0
  51. package/skills/testar-o-site/SKILL.md +46 -0
  52. package/skills/video-no-remotion/SKILL.md +48 -0
package/lib/ide.js ADDED
@@ -0,0 +1,337 @@
1
+ /**
2
+ * ide.js — a PrimoCode IDE: árvore, editor, prévia e chat, na mesma janela.
3
+ *
4
+ * "Um app ao invés de CLI, aí fica uma IDE e uma CLI."
5
+ *
6
+ * ── POR QUE MORA DENTRO DO SERVIDOR DO /rc ───────────────────────────────
7
+ * A janela do /rc já resolveu o difícil: servidor em 127.0.0.1 com chave
8
+ * sorteada, fluxo de eventos (SSE) ao vivo e o espelho da saída do terminal.
9
+ * Uma IDE com servidor próprio seria uma segunda chave, uma segunda porta e um
10
+ * segundo fluxo — e as duas janelas mostrariam agentes que parecem diferentes
11
+ * sendo o mesmo. Aqui só entram as rotas que a IDE precisa a mais (árvore,
12
+ * ler, salvar, prévia) e o vigia da pasta, que empurra eventos pelo MESMO
13
+ * fluxo do /rc.
14
+ *
15
+ * ── SEGURANÇA ────────────────────────────────────────────────────────────
16
+ * Estas rotas leem e ESCREVEM no disco do usuário. Três travas:
17
+ * · a chave do app.js vale aqui também (tudo mora em /api/);
18
+ * · todo caminho é resolvido DENTRO da pasta do projeto — "../", caminho
19
+ * absoluto e link simbólico que aponta para fora são recusados;
20
+ * · a prévia roda num iframe sandbox SEM allow-same-origin, e o servidor
21
+ * manda o mesmo sandbox no cabeçalho. O código da prévia foi escrito pelo
22
+ * agente (ou pela pessoa); se ele herdasse a origem da IDE, poderia chamar
23
+ * /api/enviar e dar ordens ao agente. Com origem opaca, o fetch dele sai
24
+ * com `Origin: null` e o app.js recusa.
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+
32
+ /* O que a árvore não mostra e o vigia não escuta. node_modules sozinho tem
33
+ dezenas de milhares de arquivos: listar travaria a janela, e vigiar faria
34
+ cada `npm install` virar uma avalanche de eventos. */
35
+ const IGNORADOS = new Set(['node_modules', '.git', '.DS_Store', '.next', '.cache',
36
+ '__pycache__', '.venv', 'venv', '.turbo', '.parcel-cache']);
37
+ const MAX_ITENS = 4000;
38
+ const MAX_PROFUNDIDADE = 12;
39
+ // 1 MB: acima disso um <textarea> engasga a cada tecla, e arquivo desse
40
+ // tamanho num projeto de iniciante é quase sempre gerado (bundle, dado).
41
+ const MAX_EDITOR = 1024 * 1024;
42
+
43
+ // ── caminhos ─────────────────────────────────────────────────────────────
44
+
45
+ /* Resolve `relativo` dentro de `raiz`, ou devolve null.
46
+ *
47
+ * `path.relative` pega o "../" e o caminho absoluto; o `realpath` pega o link
48
+ * simbólico, que passa pelo primeiro teste com cara de arquivo de dentro e
49
+ * aponta para ~/.ssh. Arquivo que ainda não existe (salvar um novo) é checado
50
+ * pela pasta onde vai nascer. */
51
+ function dentro(raiz, relativo) {
52
+ if (typeof relativo !== 'string' || relativo.includes('\0')) return null;
53
+ // Absoluto é recusado de cara, e não "reinterpretado" como relativo:
54
+ // quem manda /etc/hosts quer o /etc/hosts, e salvar em <projeto>/etc/hosts
55
+ // no lugar seria uma surpresa. `C:\` e `\\servidor` também contam.
56
+ if (path.isAbsolute(relativo) || path.win32.isAbsolute(relativo)) return null;
57
+ const base = path.resolve(raiz);
58
+ const alvo = path.resolve(base, relativo);
59
+ if (!contido(base, alvo)) return null;
60
+ let real = base;
61
+ try { real = fs.realpathSync(base); } catch { return null; }
62
+ let existente = alvo;
63
+ while (!fs.existsSync(existente) && existente !== base) existente = path.dirname(existente);
64
+ try {
65
+ if (!contido(real, fs.realpathSync(existente))) return null;
66
+ } catch { return null; }
67
+ return alvo;
68
+ }
69
+
70
+ function contido(base, alvo) {
71
+ const rel = path.relative(base, alvo);
72
+ return rel === '' || (rel !== '..' && !rel.startsWith('..' + path.sep) && !path.isAbsolute(rel));
73
+ }
74
+
75
+ function relativoDe(raiz, abs) {
76
+ return path.relative(raiz, abs).split(path.sep).join('/');
77
+ }
78
+
79
+ function ignorado(rel) {
80
+ return rel.split(/[/\\]/).some((p) => IGNORADOS.has(p));
81
+ }
82
+
83
+ // ── árvore ───────────────────────────────────────────────────────────────
84
+
85
+ /* Lista plana, pastas antes de arquivos, em ordem alfabética. Plana porque a
86
+ página monta a árvore dela do jeito que quiser, e o teste confere sem ter de
87
+ descer em objetos aninhados. */
88
+ function arvore(raiz) {
89
+ const itens = [];
90
+ let cortada = false;
91
+ function andar(dir, prof) {
92
+ if (prof > MAX_PROFUNDIDADE || cortada) return;
93
+ let entradas;
94
+ try { entradas = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
95
+ entradas = entradas.filter((e) => !IGNORADOS.has(e.name));
96
+ entradas.sort((a, b) => (b.isDirectory() - a.isDirectory())
97
+ || a.name.localeCompare(b.name, 'pt-BR', { sensitivity: 'base' }));
98
+ for (const e of entradas) {
99
+ if (itens.length >= MAX_ITENS) { cortada = true; return; }
100
+ const abs = path.join(dir, e.name);
101
+ const rel = relativoDe(raiz, abs);
102
+ if (e.isDirectory()) {
103
+ itens.push({ caminho: rel, tipo: 'pasta' });
104
+ andar(abs, prof + 1);
105
+ } else if (e.isFile()) {
106
+ let tamanho = 0;
107
+ try { tamanho = fs.statSync(abs).size; } catch {}
108
+ itens.push({ caminho: rel, tipo: 'arquivo', tamanho });
109
+ }
110
+ }
111
+ }
112
+ andar(raiz, 0);
113
+ return { itens, cortada };
114
+ }
115
+
116
+ // ── ler e salvar ─────────────────────────────────────────────────────────
117
+
118
+ /* Binário é o que tem byte zero nos primeiros 8 KB — o mesmo teste do git.
119
+ Mostrar um PNG como texto no editor e deixar a pessoa salvar por cima
120
+ destruiria a imagem sem aviso. */
121
+ function ehBinario(buf) {
122
+ const n = Math.min(buf.length, 8000);
123
+ for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
124
+ return false;
125
+ }
126
+
127
+ function ler(raiz, rel) {
128
+ const abs = dentro(raiz, rel);
129
+ if (!abs) return { status: 403, corpo: { ok: false, error: 'fora da pasta do projeto' } };
130
+ let st;
131
+ try { st = fs.statSync(abs); } catch { return { status: 404, corpo: { ok: false, error: 'não existe' } }; }
132
+ if (!st.isFile()) return { status: 400, corpo: { ok: false, error: 'não é arquivo' } };
133
+ const base = { ok: true, caminho: relativoDe(raiz, abs), tamanho: st.size, mtime: st.mtimeMs };
134
+ if (st.size > MAX_EDITOR) return { status: 200, corpo: Object.assign(base, { grande: true }) };
135
+ const buf = fs.readFileSync(abs);
136
+ if (ehBinario(buf)) return { status: 200, corpo: Object.assign(base, { binario: true }) };
137
+ return { status: 200, corpo: Object.assign(base, { conteudo: buf.toString('utf8') }) };
138
+ }
139
+
140
+ /* `mtimeBase` é o mtime que a página viu ao abrir o arquivo. Se o disco está
141
+ mais novo, alguém (quase sempre o agente) mexeu no meio — e salvar por cima
142
+ apagaria o trabalho dele sem ninguém perceber. A página decide: recarregar
143
+ ou mandar de novo com `forcar`. */
144
+ function salvar(raiz, dados) {
145
+ const rel = dados && dados.caminho;
146
+ const conteudo = dados && dados.conteudo;
147
+ if (typeof conteudo !== 'string') return { status: 400, corpo: { ok: false, error: 'conteúdo faltando' } };
148
+ if (Buffer.byteLength(conteudo) > MAX_EDITOR) return { status: 413, corpo: { ok: false, error: 'maior que 1 MB' } };
149
+ const abs = dentro(raiz, rel);
150
+ if (!abs || abs === path.resolve(raiz)) return { status: 403, corpo: { ok: false, error: 'fora da pasta do projeto' } };
151
+ if (ignorado(relativoDe(raiz, abs))) return { status: 403, corpo: { ok: false, error: 'pasta protegida' } };
152
+ let st = null;
153
+ try { st = fs.statSync(abs); } catch { /* arquivo novo */ }
154
+ if (st && !st.isFile()) return { status: 400, corpo: { ok: false, error: 'não é arquivo' } };
155
+ if (st && dados.mtimeBase && !dados.forcar && st.mtimeMs > Number(dados.mtimeBase) + 1) {
156
+ return { status: 409, corpo: { ok: false, conflito: true, error: 'mudou no disco depois que você abriu', mtime: st.mtimeMs } };
157
+ }
158
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
159
+ fs.writeFileSync(abs, conteudo);
160
+ return { status: 200, corpo: { ok: true, caminho: relativoDe(raiz, abs), mtime: fs.statSync(abs).mtimeMs } };
161
+ }
162
+
163
+ // ── prévia ───────────────────────────────────────────────────────────────
164
+
165
+ const TIPOS = {
166
+ '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
167
+ '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8',
168
+ '.mjs': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8',
169
+ '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
170
+ '.gif': 'image/gif', '.webp': 'image/webp', '.ico': 'image/x-icon', '.txt': 'text/plain; charset=utf-8',
171
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.mp4': 'video/mp4', '.webm': 'video/webm',
172
+ '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.md': 'text/plain; charset=utf-8',
173
+ };
174
+
175
+ /* Onde está a página de entrada do projeto. index.html na raiz é o caso de
176
+ quase todo pedido ("crie um site"); os kits de Vite e afins põem em public/
177
+ ou src/. Sem nenhum, a prévia diz isso em vez de mostrar um 404. */
178
+ function entradaDaPrevia(raiz) {
179
+ for (const c of ['index.html', 'public/index.html', 'src/index.html', 'dist/index.html']) {
180
+ if (fs.existsSync(path.join(raiz, c))) return c;
181
+ }
182
+ return null;
183
+ }
184
+
185
+ function servirPrevia(raiz, rel, res) {
186
+ const abs = dentro(raiz, decodeURIComponent(rel || '') || 'index.html');
187
+ const alvo = abs && fs.existsSync(abs) && fs.statSync(abs).isDirectory() ? path.join(abs, 'index.html') : abs;
188
+ if (!alvo || !fs.existsSync(alvo)) {
189
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
190
+ return res.end('não encontrado');
191
+ }
192
+ res.writeHead(200, {
193
+ 'Content-Type': TIPOS[path.extname(alvo).toLowerCase()] || 'application/octet-stream',
194
+ 'Cache-Control': 'no-store',
195
+ // O mesmo sandbox do iframe, dito pelo servidor: vale até se alguém
196
+ // abrir o endereço da prévia fora da IDE.
197
+ 'Content-Security-Policy': 'sandbox allow-scripts allow-forms allow-modals allow-popups allow-pointer-lock',
198
+ });
199
+ fs.createReadStream(alvo).pipe(res);
200
+ }
201
+
202
+ // ── vigia ────────────────────────────────────────────────────────────────
203
+
204
+ /* fs.watch recursivo existe no Mac e no Windows desde sempre e no Linux desde
205
+ o Node 20. Onde não existe, vigia só a raiz — a prévia de um index.html
206
+ continua acompanhando, que é o que mais importa.
207
+
208
+ Os eventos vêm em rajada (um write vira 2–3 avisos, um `npm run build` vira
209
+ centenas), então juntam-se por 150 ms e saem numa mensagem só. */
210
+ function vigiar(raiz, aoMudar, espera = 150) {
211
+ const pendentes = new Set();
212
+ let timer = null;
213
+ const soltar = () => {
214
+ timer = null;
215
+ const lista = [...pendentes];
216
+ pendentes.clear();
217
+ if (lista.length) aoMudar(lista);
218
+ };
219
+ const anotar = (_tipo, nome) => {
220
+ if (!nome) return;
221
+ const rel = String(nome).split(path.sep).join('/');
222
+ if (ignorado(rel)) return;
223
+ pendentes.add(rel);
224
+ if (!timer) timer = setTimeout(soltar, espera);
225
+ };
226
+ let vigia;
227
+ try { vigia = fs.watch(raiz, { recursive: true }, anotar); } catch {
228
+ try { vigia = fs.watch(raiz, anotar); } catch { return () => {}; }
229
+ }
230
+ vigia.on('error', () => {});
231
+ return function parar() {
232
+ try { vigia.close(); } catch {}
233
+ if (timer) clearTimeout(timer);
234
+ };
235
+ }
236
+
237
+ // ── as rotas ─────────────────────────────────────────────────────────────
238
+
239
+ /**
240
+ * Monta a IDE num servidor do app.js.
241
+ *
242
+ * @param {object} op
243
+ * @param {()=>string} op.pasta a pasta do projeto AGORA (o /dir muda ela)
244
+ * @param {(evento:string, dados:any)=>void} op.emitir o fluxo SSE do app.js
245
+ * @returns {{atender:Function, fechar:Function, conferirPasta:Function}}
246
+ */
247
+ function montar(op) {
248
+ let pastaVigiada = null;
249
+ let pararVigia = () => {};
250
+ // O que a própria IDE acabou de salvar, para a página não pintar como
251
+ // "o agente mexeu" a mudança que a pessoa fez.
252
+ const salvosAgora = new Map();
253
+
254
+ function conferirPasta() {
255
+ const atual = op.pasta && op.pasta();
256
+ if (!atual || atual === pastaVigiada) return atual;
257
+ pararVigia();
258
+ pastaVigiada = atual;
259
+ if (!fs.existsSync(atual)) { pararVigia = () => {}; return atual; }
260
+ pararVigia = vigiar(atual, (mudados) => {
261
+ const agora = Date.now();
262
+ const deVoce = mudados.filter((m) => (salvosAgora.get(m) || 0) > agora - 1500);
263
+ op.emitir('arquivos', { pasta: atual, mudados, deVoce });
264
+ });
265
+ op.emitir('arquivos', { pasta: atual, mudados: [], trocou: true });
266
+ return atual;
267
+ }
268
+ // O /dir troca a pasta pelo terminal, sem passar por aqui: sem esta volta
269
+ // a IDE continuaria vigiando o projeto anterior.
270
+ /* E só depois que a IDE foi aberta de verdade: quem abriu só o chat do
271
+ /rc não precisa de ninguém vigiando o ~/primocode inteiro — no Linux
272
+ o fs.watch recursivo custa um aviso do sistema por pasta. */
273
+ let usada = false;
274
+ const ronda = setInterval(() => { if (usada) conferirPasta(); }, 2000);
275
+ if (ronda.unref) ronda.unref();
276
+
277
+ function json(res, status, corpo) {
278
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
279
+ res.end(JSON.stringify(corpo));
280
+ }
281
+
282
+ function lerCorpo(req) {
283
+ return new Promise((ok) => {
284
+ let corpo = '';
285
+ let grande = false;
286
+ req.on('data', (c) => { corpo += c; if (corpo.length > MAX_EDITOR * 2) { grande = true; req.destroy(); } });
287
+ req.on('end', () => { try { ok(grande ? null : JSON.parse(corpo)); } catch { ok(null); } });
288
+ });
289
+ }
290
+
291
+ /* Devolve true se a rota era da IDE. A chave já foi conferida pelo
292
+ app.js antes de chegar aqui — só as rotas com /api/ passam por ela, e
293
+ por isso a prévia também mora debaixo de /api/. */
294
+ async function atender(req, res, url) {
295
+ const rota = url.pathname;
296
+ if (!rota.startsWith('/api/ide/')) return false;
297
+ usada = true;
298
+ const raiz = conferirPasta();
299
+ if (!raiz || !fs.existsSync(raiz)) { json(res, 404, { ok: false, error: 'sem pasta de projeto' }); return true; }
300
+
301
+ if (rota === '/api/ide/arvore') {
302
+ const a = arvore(raiz);
303
+ json(res, 200, { ok: true, pasta: raiz, nome: path.basename(raiz), itens: a.itens,
304
+ cortada: a.cortada, entrada: entradaDaPrevia(raiz) });
305
+ return true;
306
+ }
307
+ if (rota === '/api/ide/arquivo') {
308
+ const r = ler(raiz, url.searchParams.get('p') || '');
309
+ json(res, r.status, r.corpo);
310
+ return true;
311
+ }
312
+ if (rota === '/api/ide/salvar' && req.method === 'POST') {
313
+ const dados = await lerCorpo(req);
314
+ if (!dados) { json(res, 400, { ok: false, error: 'pedido ilegível ou maior que 1 MB' }); return true; }
315
+ const r = salvar(raiz, dados);
316
+ if (r.corpo.ok) salvosAgora.set(r.corpo.caminho, Date.now());
317
+ json(res, r.status, r.corpo);
318
+ return true;
319
+ }
320
+ /* /api/ide/previa/<chave>/<caminho>: a chave vai no CAMINHO, não na
321
+ query, porque a página da prévia pede style.css e script.js por
322
+ endereço relativo — e o relativo herda o caminho, não a query. */
323
+ const m = rota.match(/^\/api\/ide\/previa\/[0-9a-f]+\/?(.*)$/);
324
+ if (m) { servirPrevia(raiz, m[1], res); return true; }
325
+
326
+ json(res, 404, { ok: false, error: 'rota desconhecida' });
327
+ return true;
328
+ }
329
+
330
+ return {
331
+ atender,
332
+ conferirPasta,
333
+ fechar() { clearInterval(ronda); pararVigia(); },
334
+ };
335
+ }
336
+
337
+ module.exports = { montar, dentro, arvore, ler, salvar, vigiar, ehBinario, entradaDaPrevia, MAX_EDITOR };
package/lib/kits.js CHANGED
@@ -1,25 +1,24 @@
1
1
  /**
2
- * kits.js — o modelo grátis começa de uma página pronta, e não do zero.
2
+ * kits.js — a IA recebe uma direção visual para se INSPIRAR, e não um molde.
3
3
  *
4
4
  * "A IA free fez essa página, ficou boa, mas quero que ela seja mais
5
- * rápida." · "Como chegar nesse nível com os modelos free? Nem terminou,
6
- * demora 10 anos." (24/09/2026)
5
+ * rápida." · "Mas os sites vão ficar todos iguais?" · "Eu quero que tenha
6
+ * modelo pra IA só se inspirar, não pra ela mudar só texto." (24/09/2026)
7
7
  *
8
- * ── A CONTA ──────────────────────────────────────────────────────────────
9
- * Uma landing boa são ~30 KB de HTML+CSS+JS — uns 9 mil tokens de SAÍDA. No
10
- * plano grátis (6 mil tokens por minuto, entrada e saída juntas) isso não
11
- * cabe numa resposta: o servidor manda "uma tool por vez", e a página sai em
12
- * pedaços, com espera a cada minuto. E o modelo fraco, desenhando do zero,
13
- * erra justamente o que dá o nível: tipografia, espaço, animação.
8
+ * ── A HISTÓRIA, EM TRÊS PASSOS ───────────────────────────────────────────
9
+ * 1. Primeiro o kit era COPIADO para a pasta e o agente só trocava texto e
10
+ * cor: rápido no plano grátis (1–2 mil tokens de saída em vez de ~9 mil),
11
+ * mas todo site saía com o mesmo esqueleto.
12
+ * 2. Viraram cinco direções com estruturas diferentes. Ainda eram moldes.
13
+ * 3. Agora é INSPIRAÇÃO: nada é copiado. O agente recebe a ficha da direção
14
+ * que combina com o negócio (paleta, fontes, layout, o elemento marcante,
15
+ * o que evitar) e escreve a página dele. Custa mais saída que trocar
16
+ * texto — o preço de cada site sair com a cara do seu negócio — e a
17
+ * instrução manda escrever o CSS em partes para cada resposta caber na
18
+ * cota.
14
19
  *
15
- * Com o kit, o design já está resolvido (a landing que o Claude fez, com o
16
- * conteúdo trocado por marcadores). O agente só TROCA texto e três cores com
17
- * edit_file: ~1–2 mil tokens de saída em vez de 9 mil. Mais rápido E melhor,
18
- * pela mesma razão — o trabalho difícil já foi feito uma vez, por quem faz
19
- * bem.
20
- *
21
- * Só entra em pasta NOVA (ou vazia): num projeto que já tem página, copiar
22
- * um kit por cima seria apagar o trabalho da pessoa.
20
+ * As páginas completas de kits/<nome>/ ficam como a prova de que cada
21
+ * direção funciona: foram renderizadas e conferidas, e as fichas saíram delas.
23
22
  */
24
23
 
25
24
  'use strict';
@@ -35,22 +34,153 @@ const RE_LANDING = /\b(landing|lading|p[áa]gina (de vendas|de captura|inicial|i
35
34
  // Pedido que não quer a cara do kit: nada de puxar o kit por cima disso.
36
35
  const RE_SEM_KIT = /\b(do zero|sem modelo|sem template|sem kit|react|next(\.?js)?|vue|svelte|angular|tailwind|wordpress)\b/i;
37
36
 
37
+ /* ── INSPIRAÇÃO, NÃO MOLDE ────────────────────────────────────────────────
38
+ * "Eu quero que tenha modelo pra IA só se INSPIRAR, não pra ela mudar só
39
+ * texto." (24/09/2026)
40
+ * Os kits eram copiados para a pasta e o agente só trocava texto e cor — e
41
+ * todo site saía com o mesmo esqueleto. Agora nada é copiado: o agente recebe
42
+ * uma FICHA da direção que combina com o negócio (paleta, fontes, ideia de
43
+ * layout, o elemento marcante, o movimento, o que evitar) e escreve a página
44
+ * DELE. A ficha é curta (~1,5 mil caracteres) para caber na cota grátis. As
45
+ * páginas completas continuam em kits/<nome>/ como a prova de que cada
46
+ * direção funciona — é delas que as fichas foram tiradas. */
47
+ function instrucaoDoKit(kit) {
48
+ return `INSPIRAÇÃO VISUAL (ponto de partida, NÃO é molde): direção "${kit.titulo}".\n${kit.ficha}\n\n`
49
+ + 'Crie a página DESTE negócio do zero (index.html, style.css e, se precisar, script.js), com o conteúdo '
50
+ + 'real do pedido. Use a direção acima como inspiração: pode e deve mudar a estrutura, as seções, as cores '
51
+ + 'e as fontes para caber no negócio — duas páginas feitas com esta ficha não devem sair iguais. Não copie '
52
+ + 'textos de exemplo e não invente fato que você não sabe. Antes de escrever, decida em 3 linhas o que '
53
+ + 'será o elemento marcante desta página. Arquivo grande: escreva o CSS em partes (a base primeiro, os '
54
+ + 'detalhes depois com edit_file), para cada resposta caber.';
55
+ }
56
+
57
+ /* Uma família de direções, e não uma só: "mas os sites vão ficar todos
58
+ iguais?" (24/09/2026). Cada uma pede uma ESTRUTURA diferente. */
38
59
  const KITS = {
39
60
  landing: {
40
61
  nome: 'landing',
41
- arquivos: ['index.html', 'style.css', 'script.js'],
42
- instrucao: 'A PASTA JÁ TEM UMA LANDING PRONTA (index.html, style.css, script.js), com design profissional, '
43
- + 'animações e responsiva. NÃO reescreva os arquivos com write_file: é lento e piora o design. '
44
- + 'ADAPTE com edit_file: (1) leia o index.html; (2) troque cada bloco marcado com <!-- TROQUE --> pelo '
45
- + 'conteúdo do pedido — textos reais, específicos do negócio, sem inventar fato que você não sabe; '
46
- + '(3) no topo do style.css troque só as cores --blue, --orange, --cyan (e as -deep) pelas cores que '
47
- + 'combinam com o negócio; (4) troque também as FONTES (o link do Google Fonts no index.html e --sans/'
48
- + '--serif no style.css) por um par que tenha a cara DESTE negócio; (5) tire o que não informa nada: '
49
- + 'rótulos em caixa alta sobre os títulos, números 01/02/03 quando não é sequência; (6) apague os '
50
- + 'comentários TROQUE. Pode remover uma seção que não faz sentido para o negócio (e o link dela no '
51
- + 'menu). Não mexa no script.js.',
62
+ titulo: 'escura e tecnológica',
63
+ ficha: '- para: tecnologia, app, startup, barbearia, tatuagem, academia — negócio de noite ou de futuro.\n'
64
+ + '- paleta: fundo quase preto azulado (#0b0b12), um azul profundo (#1e3fae), um laranja de destaque '
65
+ + '(#f97316), um ciano de apoio (#1fb4cf).\n'
66
+ + '- fontes: um sans geométrico e firme para tudo (ex.: Instrument Sans, Space Grotesk, Sora) e, se '
67
+ + 'quiser, uma serifa só para um detalhe.\n'
68
+ + '- layout: abertura de tela cheia centrada, com fundo vivo (rede de pontos ou brilho que segue o '
69
+ + 'mouse, em canvas ou CSS); depois seções escuras largas.\n'
70
+ + '- marcante: o fundo animado da abertura. O resto fica sóbrio.\n'
71
+ + '- evite: fade em toda seção, números 01/02/03, rótulo em caixa alta sobre todo título.',
72
+ },
73
+ editorial: {
74
+ nome: 'editorial',
75
+ titulo: 'editorial clara',
76
+ ficha: '- para: advocacia, consultoria, contabilidade, fotografia, curso, mentoria, agência.\n'
77
+ + '- paleta: papel quase branco (#fcfcfb), tinta quase preta (#141414), UM azul forte (#1432c8) usado '
78
+ + 'pouco, e um azul bem claro (#e7ebfb) para uma faixa.\n'
79
+ + '- fontes: um título condensado e pesado (ex.: Archivo condensada, Oswald, Anton) + uma serifa de '
80
+ + 'leitura para o texto (ex.: Newsreader, Source Serif, Literata).\n'
81
+ + '- layout: tudo alinhado à ESQUERDA numa grade assimétrica (coluna estreita de rótulo + coluna larga '
82
+ + 'de conteúdo); serviços em LINHAS separadas por filete, não em cards; um depoimento grande em itálico.\n'
83
+ + '- marcante: o título da abertura, enorme e apertado, ocupando metade da tela.\n'
84
+ + '- movimento: quase nenhum. Formulário só com a linha de baixo nos campos.',
85
+ },
86
+ acolhedor: {
87
+ nome: 'acolhedor',
88
+ titulo: 'acolhedora de bairro',
89
+ ficha: '- para: padaria, confeitaria, salão, clínica, dentista, pet, escola, creche — o negócio da esquina.\n'
90
+ + '- paleta: fundo creme quente (#fff9f2), um verde acolhedor (#2f7a5f), amarelo sol (#ffc247) e um '
91
+ + 'coral (#f28b74). Troque pelo que combinar com o ramo.\n'
92
+ + '- fontes: um título redondo e simpático (ex.: Fredoka, Baloo 2, Nunito ExtraBold) + um texto '
93
+ + 'arredondado e legível (ex.: Nunito, Quicksand).\n'
94
+ + '- layout: texto à esquerda e, à direita, uma ILUSTRAÇÃO feita só com CSS que lembra o negócio '
95
+ + '(lojinha com toldo listrado, bolo, patinha, dente) — nada de imagem de fora; quadro de preços com '
96
+ + 'pontilhado ligando nome e preço; falas de cliente em balões; botão de WhatsApp em destaque.\n'
97
+ + '- marcante: um selo "Aberto agora, até as 18h" calculado pelo horário real (script.js).\n'
98
+ + '- tudo arredondado (raios grandes), nada de fundo escuro.',
99
+ },
100
+ vitrine: {
101
+ nome: 'vitrine',
102
+ titulo: 'vitrine colorida',
103
+ ficha: '- para: comida, pizzaria, hamburgueria, açaí, evento, festa, loja, lançamento de produto.\n'
104
+ + '- paleta: cores chapadas e vivas em BLOCOS: amarelo (#ffd23f), rosa (#ff4f8b), azul elétrico '
105
+ + '(#2d5bff), preto (#141414). Contorno grosso preto entre os blocos.\n'
106
+ + '- fontes: um grotesco bem pesado e expressivo para títulos GIGANTES (ex.: Bricolage Grotesque, '
107
+ + 'Archivo Black, Rubik) — pode ser a mesma família no texto.\n'
108
+ + '- layout: cartaz de rua. Abertura em um bloco de cor com título que quase estoura a tela; faixa '
109
+ + 'com quando/onde/preço; MOSAICO de produtos com tamanhos diferentes (um grande, vários pequenos), '
110
+ + 'cada um com etiqueta de preço inclinada; chamada final enorme ("Bora?").\n'
111
+ + '- marcante: um selo serrilhado girado ("Só nesta semana") e, se houver data, a contagem em '
112
+ + 'palavras ("Faltam 12 dias").\n'
113
+ + '- evite: cards iguais com sombra suave e degradê.',
114
+ },
115
+ premium: {
116
+ nome: 'premium',
117
+ titulo: 'minimalista premium',
118
+ ficha: '- para: arquitetura, joalheria, imobiliária, hotel, spa, estética, vinho — serviço de alto padrão.\n'
119
+ + '- paleta: fundo pedra clara (#f4f4f1), um verde muito escuro (#1f3a34), um dourado envelhecido '
120
+ + '(#b8955a) só em fios e detalhes, e um cinza quente (#dcd9d2).\n'
121
+ + '- fontes: uma serifa de alto contraste, fina e grande, para títulos (ex.: Bodoni Moda, Playfair '
122
+ + 'Display, Cormorant) + um sans discreto e pequeno para o texto (ex.: Manrope, Inter).\n'
123
+ + '- layout: tudo CENTRADO, com MUITO espaço vazio; poucas seções e curtas; três frentes de trabalho '
124
+ + 'lado a lado só com um fio fino em cima; depoimento curto; contato com hora marcada.\n'
125
+ + '- marcante: UM elemento só na abertura (ex.: um arco de cor escura com um fio dourado e uma luz), '
126
+ + 'que se mexe uma vez ao abrir. Nada mais se mexe.\n'
127
+ + '- botões finos, só contorno. Antes de terminar, tire um enfeite.',
52
128
  },
53
129
  };
130
+ for (const kit of Object.values(KITS)) kit.instrucao = instrucaoDoKit(kit);
131
+
132
+ /* Tira acento e caixa: "Clínica", "clinica" e "CLÍNICA" viram a mesma
133
+ coisa, e o \b do JavaScript volta a funcionar (ele não entende "í"). */
134
+ function normalizar(texto) {
135
+ return String(texto || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
136
+ }
137
+
138
+ /* Pedido explícito de estilo vence o ramo do negócio: quem diz "estilo
139
+ editorial" para a padaria quer a editorial. */
140
+ const ESTILO_PEDIDO = [
141
+ ['editorial', /\b(editorial|estilo (de )?revista|cara de revista|jornal[ií]stic[oa])\b/],
142
+ ['acolhedor', /\b(acolhedor[a]?|aconchegante|fofo|fofa|amigavel|caseir[oa])\b/],
143
+ ['vitrine', /\b(vitrine|colorid[oa]|vibrante|chamativ[oa]|divertid[oa]|bold|cartaz)\b/],
144
+ ['premium', /\b(premium|minimalista|minimal|sofisticad[oa]|elegante|luxuos[oa]|de luxo|alto padrao)\b/],
145
+ ['landing', /\b(escur[oa]|dark|futurista|tecnologic[oa]|neon)\b/],
146
+ ];
147
+
148
+ /* O ramo do negócio → a direção que combina. Ganha a palavra que aparece
149
+ PRIMEIRO no pedido ("app da padaria" é sobre o app). */
150
+ const RAMO = [
151
+ ['acolhedor', /\b(padaria|confeitaria|doceria|salao|cabeleireir[oa]|manicure|clinica|dentista|odonto\w*|consultorio|pet ?shop|pet|veterinari[oa]|escola|creche|bercario|psicolog[oa]|fisioterap\w*|nutricionista|lavanderia|floricultura|mercadinho|costureira|artesanato)\b/],
152
+ ['vitrine', /\b(evento|festa|show|festival|comida|lanchonete|lanche|hamburgueria|hamburguer|pizzaria|pizza|sorveteria|acai|food ?truck|delivery|loja|produto|lancamento|promocao|brecho|bar|cervejaria|churrasco|doces?)\b/],
153
+ ['editorial', /\b(advocacia|advogad[oa]|consultoria|contabilidade|contador[a]?|jornal|revista|blog|podcast|fotograf[oa]|fotografia|escritor[a]?|livro|palestrante|mentoria|curso|agencia)\b/],
154
+ ['premium', /\b(arquitetura|arquitet[oa]|joalheria|joias?|imobiliaria|imoveis|corretor[a]?|interiores|hotel|pousada|spa|vinho|vinicola|estetica|relojoaria|alfaiataria|perfumaria)\b/],
155
+ ['landing', /\b(tech|startup|app|aplicativo|software|saas|ia|inteligencia artificial|plataforma|games?|barbearia|tattoo|tatuagem|estudio|academia|crossfit)\b/],
156
+ ];
157
+
158
+ /* Sem pista nenhuma, a escolha RODA pelo texto do pedido: o mesmo pedido cai
159
+ sempre no mesmo kit (dá para reproduzir), mas dois pedidos diferentes não
160
+ caem sempre no mesmo — que era exatamente a reclamação. */
161
+ const ORDEM_RODIZIO = ['editorial', 'acolhedor', 'vitrine', 'premium', 'landing'];
162
+ function hashDoPedido(texto) {
163
+ let h = 5381;
164
+ for (const ch of normalizar(texto).trim()) h = ((h * 33) ^ ch.codePointAt(0)) >>> 0;
165
+ return h;
166
+ }
167
+
168
+ function primeiraPista(t, tabela) {
169
+ let melhor = null;
170
+ for (const [nome, re] of tabela) {
171
+ const m = re.exec(t);
172
+ if (m && (!melhor || m.index < melhor.pos)) melhor = { nome, pos: m.index };
173
+ }
174
+ return melhor && melhor.nome;
175
+ }
176
+
177
+ /** Qual direção visual combina com o pedido (sem as regras de quando usar kit). */
178
+ function direcao(pedido) {
179
+ const t = normalizar(pedido);
180
+ return primeiraPista(t, ESTILO_PEDIDO)
181
+ || primeiraPista(t, RAMO)
182
+ || ORDEM_RODIZIO[hashDoPedido(pedido) % ORDEM_RODIZIO.length];
183
+ }
54
184
 
55
185
  /* O kit é para quem NÃO consegue desenhar bem sozinho no tempo da cota. O
56
186
  motor forte (Claude Code, Codex, a chave do Claude ou do ChatGPT) desenha do
@@ -62,34 +192,15 @@ function motorForte(provedorAtual) {
62
192
  return Boolean(provedorAtual && (provedorAtual.cli || FORTES.has(provedorAtual.id)));
63
193
  }
64
194
 
65
- /** Qual kit este pedido quer, ou null. */
195
+ /** Qual kit este pedido quer, ou null. A direção sai do negócio (ou do estilo pedido). */
66
196
  function escolher(pedido, { provedor = null } = {}) {
67
- if (motorForte(provedor)) return null;
197
+ // A ficha é inspiração, não molde: vale para qualquer motor. O forte usa
198
+ // de partida junto da skill design-de-verdade; o grátis ganha uma direção
199
+ // em vez de inventar do zero. (`provedor` fica para quem quiser distinguir.)
200
+ void provedor;
68
201
  const t = String(pedido || '');
69
202
  if (!RE_LANDING.test(t) || RE_SEM_KIT.test(t)) return null;
70
- return KITS.landing;
71
- }
72
-
73
- /** A pasta está livre para receber o kit? (não existe, ou só tem memory.md) */
74
- function pastaLivre(dir) {
75
- try {
76
- const tem = fs.readdirSync(dir).filter((n) => !n.startsWith('.') && n !== 'memory.md');
77
- return tem.length === 0;
78
- } catch { return true; }
79
- }
80
-
81
- /** Copia o kit para `dir`. Devolve os arquivos copiados, ou [] se não copiou. */
82
- function aplicar(kit, dir) {
83
- if (!kit || !pastaLivre(dir)) return [];
84
- fs.mkdirSync(dir, { recursive: true });
85
- const feitos = [];
86
- for (const nome of kit.arquivos) {
87
- const destino = path.join(dir, nome);
88
- if (fs.existsSync(destino)) continue;
89
- fs.copyFileSync(path.join(PASTA_KITS, kit.nome, nome), destino);
90
- feitos.push(nome);
91
- }
92
- return feitos;
203
+ return KITS[direcao(t)];
93
204
  }
94
205
 
95
- module.exports = { KITS, PASTA_KITS, escolher, pastaLivre, aplicar, motorForte };
206
+ module.exports = { KITS, PASTA_KITS, escolher, direcao, normalizar, motorForte, instrucaoDoKit };
package/lib/skills.js CHANGED
@@ -345,9 +345,14 @@ const semAcento = (v) => String(v || '').toLowerCase()
345
345
  function pontuar(skill, texto) {
346
346
  const t = semAcento(texto);
347
347
  let pontos = 0;
348
+ // "página, pagina" no gatilho é UMA palavra escrita de dois jeitos: sem
349
+ // acento as duas são iguais e contavam dois pontos, e a skill que listava
350
+ // as duas grafias ganhava das outras só por isso.
351
+ const vistas = new Set();
348
352
  for (const palavra of skill.quando) {
349
353
  const p = semAcento(palavra).trim();
350
- if (!p) continue;
354
+ if (!p || vistas.has(p)) continue;
355
+ vistas.add(p);
351
356
  const escapado = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
352
357
  // A borda e escrita a mao em vez de `\b`: o gatilho pode comecar ou
353
358
  // terminar em simbolo ("e-mail"), e ali o `\b` nao vale.