primocode 9.8.0-beta.5 → 9.8.0-beta.7

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 +338 -88
  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/act.js +66 -7
  20. package/lib/app.js +41 -6
  21. package/lib/browser.js +79 -3
  22. package/lib/checkpoint-git.js +218 -0
  23. package/lib/checkpoint.js +159 -44
  24. package/lib/comandos-extra.js +481 -0
  25. package/lib/config.js +5 -2
  26. package/lib/ide.js +337 -0
  27. package/lib/kits.js +165 -54
  28. package/lib/pre-leitura.js +118 -0
  29. package/lib/repeticao.js +4 -2
  30. package/lib/skills.js +6 -1
  31. package/lib/tempos.js +62 -0
  32. package/lib/ui.js +171 -17
  33. package/package.json +2 -2
  34. package/skills/apresentacao-pptx/SKILL.md +52 -0
  35. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  36. package/skills/cartaz-de-arte/SKILL.md +47 -0
  37. package/skills/criar-skill/LICENSE.txt +202 -0
  38. package/skills/criar-skill/SKILL.md +49 -0
  39. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  40. package/skills/depurar-de-verdade/SKILL.md +14 -5
  41. package/skills/documento-word/SKILL.md +57 -0
  42. package/skills/interface-revisada/SKILL.md +47 -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
@@ -0,0 +1,118 @@
1
+ /**
2
+ * pre-leitura.js — os arquivos que o agente ia ler, já lidos.
3
+ *
4
+ * "Toda vez a IA lê as pastas novamente, só pra ler vão 30 chamadas de API."
5
+ * (dono, 24/09/2026)
6
+ *
7
+ * O caro não é o disco — ler index.html leva 1ms. O caro é a IDA AO MODELO: o
8
+ * servidor grátis conta pedidos por dia, e cada "deixa eu ler o style.css"
9
+ * numa resposta própria é um pedido. Medido com um modelo de mentira que
10
+ * segue uma política fixa (lê o que ainda não viu, um arquivo por resposta,
11
+ * e confere o index.html antes de editar), num "muda a cor do botão" em
12
+ * projeto de 10 arquivos: 9 pedidos ao modelo antes, 2 depois. Com todos os
13
+ * arquivos grandes demais para entrar aqui, a ordem de juntar leituras e a
14
+ * releitura sem mudança (lib/act.js, RELEITURA) ainda levam de 9 para 4.
15
+ *
16
+ * O mapa do projeto já dava os NOMES. Isto põe o CONTEÚDO dos poucos arquivos
17
+ * que quase todo pedido de página lê — o citado no pedido primeiro, depois
18
+ * index.html, o css e o js principais, package.json, README — dentro de um
19
+ * orçamento. Pequeno pelo servidor grátis (o teto é de ~6 mil tokens por
20
+ * MINUTO, e isto vai em toda rodada); folgado com um provedor próprio.
21
+ *
22
+ * Fica de fora o que não se lê como texto ou não vale o espaço: binário,
23
+ * minificado, lockfile, arquivo grande, e o que já entra por outro caminho
24
+ * (memory.md, PRIMO.md/AGENTS.md/CLAUDE.md). Arquivo que não cabe inteiro no
25
+ * que sobrou do orçamento NÃO entra cortado: meio arquivo marcado como
26
+ * "conteúdo atual" faria o modelo editar às cegas a metade que não viu.
27
+ *
28
+ * Puro, menos a leitura do disco — o teste mede a escolha sem rede.
29
+ */
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+
35
+ const ORCAMENTO_SERVIDOR = 6 * 1024;
36
+ const ORCAMENTO_LOCAL = 40 * 1024;
37
+ const MAIOR_ARQUIVO = 20 * 1024;
38
+
39
+ const TEXTO = /\.(html?|css|scss|m?jsx?|tsx?|vue|svelte|json|md|txt|py|toml|ya?ml)$/i;
40
+ const LOCKFILES = new Set(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb',
41
+ 'composer.lock', 'Cargo.lock', 'poetry.lock', 'Gemfile.lock']);
42
+ // Já entram no contexto por outro caminho (buildMemoryContext / mencoes.js).
43
+ const JA_ENTRAM = new Set(['memory.md', 'PRIMO.md', 'AGENTS.md', 'CLAUDE.md']);
44
+
45
+ /** O orçamento de agora: folgado só com provedor próprio (/api). */
46
+ function orcamento() {
47
+ try { return require('./local/provedor.js').atual() ? ORCAMENTO_LOCAL : ORCAMENTO_SERVIDOR; }
48
+ catch { return ORCAMENTO_SERVIDOR; }
49
+ }
50
+
51
+ /** Quanto o arquivo importa: menor é mais importante; null = não entra. */
52
+ function prioridade(rel, pedido) {
53
+ const nome = path.basename(rel);
54
+ if (JA_ENTRAM.has(nome) || LOCKFILES.has(nome) || /\.min\./i.test(nome) || !TEXTO.test(nome)) return null;
55
+ const fundo = rel.split(/[\\/]/).length - 1;
56
+ const texto = String(pedido || '').toLowerCase();
57
+ // Citado no pedido ("muda o style.css") é o que ele vai ler com certeza.
58
+ if (texto && (texto.includes(rel.toLowerCase()) || texto.includes(nome.toLowerCase()))) return 0;
59
+ if (fundo > 1) return null;
60
+ if (/^index\.html?$/i.test(nome)) return 1 + fundo;
61
+ if (/\.(css|scss)$/i.test(nome)) return 3 + fundo;
62
+ if (/\.(m?jsx?|tsx?|vue|svelte)$/i.test(nome) && !/\.(test|spec|config)\./i.test(nome)) return 5 + fundo;
63
+ if (nome === 'package.json' && fundo === 0) return 7;
64
+ if (/^readme\.md$/i.test(nome) && fundo === 0) return 8;
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Escolhe e lê. Devolve [{ rel, conteudo }] na ordem de importância.
70
+ * @param {string} dir a pasta do projeto
71
+ * @param {string[]} arquivos caminhos relativos (o mesmo que o mapa listou)
72
+ * @param {string} pedido o texto do pedido — o citado nele vem primeiro
73
+ * @param {number} limite bytes no total
74
+ */
75
+ function escolher(dir, arquivos, pedido, limite = orcamento()) {
76
+ const candidatos = (arquivos || [])
77
+ .map((rel) => ({ rel, p: prioridade(rel, pedido) }))
78
+ .filter((x) => x.p !== null)
79
+ .sort((a, b) => a.p - b.p || a.rel.localeCompare(b.rel));
80
+ const lidos = [];
81
+ let soma = 0;
82
+ for (const { rel } of candidatos) {
83
+ const abs = path.join(dir, rel);
84
+ let st;
85
+ try { st = fs.statSync(abs); } catch { continue; }
86
+ if (!st.isFile() || st.size === 0 || st.size > MAIOR_ARQUIVO || soma + st.size > limite) continue;
87
+ let buf;
88
+ try { buf = fs.readFileSync(abs); } catch { continue; }
89
+ if (buf.includes(0)) continue; // binário com nome de texto
90
+ const conteudo = buf.toString('utf-8');
91
+ // Uma linha de milhares de caracteres é minificado sem ".min" no nome.
92
+ if (conteudo.split('\n').some((l) => l.length > 2000)) continue;
93
+ lidos.push({ rel, conteudo });
94
+ soma += st.size;
95
+ }
96
+ return lidos;
97
+ }
98
+
99
+ /** O bloco que vai no contexto, ou '' quando não há nada que valha. */
100
+ function bloco(lidos) {
101
+ if (!lidos || !lidos.length) return '';
102
+ const partes = lidos.map(({ rel, conteudo }) =>
103
+ `[conteúdo atual de ${rel} — não precisa ler de novo]\n${conteudo.replace(/\s+$/, '')}\n[fim de ${rel}]`);
104
+ return 'Estes arquivos estão aqui como estavam no COMEÇO deste pedido — use o que está abaixo em vez de '
105
+ + 'chamar read_file neles. Depois que você editar um deles, o texto abaixo fica velho: aí, sim, leia.\n\n'
106
+ + partes.join('\n\n');
107
+ }
108
+
109
+ /* A ordem de juntar. Vai junto do mapa porque é ali que o modelo decide o que
110
+ ler. Cada resposta dele é uma ida ao servidor; cinco leituras em cinco
111
+ respostas custam cinco pedidos, e as mesmas cinco numa resposta só custam
112
+ um. */
113
+ const JUNTAR_LEITURAS = 'Precisa ler, listar ou buscar mais de uma coisa? Peça TUDO na MESMA resposta, '
114
+ + 'com várias chamadas de ferramenta juntas — nunca uma leitura por resposta. Cada resposta sua é '
115
+ + 'uma ida ao servidor, e o plano conta idas.';
116
+
117
+ module.exports = { escolher, bloco, prioridade, orcamento, JUNTAR_LEITURAS,
118
+ ORCAMENTO_SERVIDOR, ORCAMENTO_LOCAL, MAIOR_ARQUIVO };
package/lib/repeticao.js CHANGED
@@ -173,9 +173,11 @@ function ver(registro, nome, args) {
173
173
  }
174
174
 
175
175
  registro.vistos.set(chave, vezes);
176
- if (vezes === 1) return { repetida: false, vezes, aviso: null };
176
+ // `chave` volta para quem chamou guardar o resultado da leitura pela
177
+ // mesma época em que ela foi vista (lib/act.js, RELEITURA).
178
+ if (vezes === 1) return { repetida: false, vezes, aviso: null, chave };
177
179
 
178
- return { repetida: true, vezes, aviso: aviso(nome, vezes, parecidaCom) };
180
+ return { repetida: true, vezes, aviso: aviso(nome, vezes, parecidaCom), chave };
179
181
  }
180
182
 
181
183
  /** O recado, apertando a cada volta. */
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.
package/lib/tempos.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * tempos.js — para onde foi o tempo de um pedido.
3
+ *
4
+ * PRIMOCODE_TEMPOS=1 primo
5
+ *
6
+ * No fim de cada pedido sai UMA linha com o que custou cada fase: o plano, a
7
+ * espera do resumo, o nome da pasta, cada rodada do modelo, as ferramentas e
8
+ * a conferência da página. Existe porque "está lento" sem número vira chute:
9
+ * medido em 24/09 com um servidor de mentira que responde na hora, o que o
10
+ * PrimoCode gastava sozinho num pedido de página era quase todo a conferência
11
+ * no navegador escondido (2,1 a 4,6s num Chrome recém-aberto), e não o que
12
+ * parecia: as ferramentas de três rodadas somaram 7 a 23ms, e o planejador
13
+ * nem é chamado em pedido de uma fase só.
14
+ *
15
+ * Desligado, não custa nada: `medir` chama a função e devolve, sem relógio.
16
+ *
17
+ * Um pedido por vez (é assim que o CLI trabalha), então o registro é do
18
+ * módulo. Subagente soma no mesmo pedido, com o nome da fase.
19
+ */
20
+ 'use strict';
21
+
22
+ let atual = null;
23
+
24
+ function ligado() { return process.env.PRIMOCODE_TEMPOS === '1'; }
25
+
26
+ /** Começa o registro de um pedido. Desligado, não faz nada. */
27
+ function comecar() {
28
+ atual = ligado() ? { inicio: Date.now(), fases: new Map() } : null;
29
+ return atual;
30
+ }
31
+
32
+ /** Soma `ms` à fase `nome` (várias rodadas somam na mesma linha, com contagem). */
33
+ function somar(nome, ms) {
34
+ if (!atual) return;
35
+ const f = atual.fases.get(nome) || { ms: 0, vezes: 0 };
36
+ f.ms += ms;
37
+ f.vezes++;
38
+ atual.fases.set(nome, f);
39
+ }
40
+
41
+ /** Mede uma função (síncrona ou async) e soma o tempo à fase. */
42
+ async function medir(nome, fn) {
43
+ if (!atual) return fn();
44
+ const t = Date.now();
45
+ try { return await fn(); } finally { somar(nome, Date.now() - t); }
46
+ }
47
+
48
+ function fmt(ms) { return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`; }
49
+
50
+ /** A linha do fim. Encerra o registro. null se estava desligado. */
51
+ function relatorio() {
52
+ if (!atual) return null;
53
+ const total = Date.now() - atual.inicio;
54
+ const partes = [...atual.fases].map(([nome, f]) =>
55
+ `${nome} ${fmt(f.ms)}${f.vezes > 1 ? ` (${f.vezes}x)` : ''}`);
56
+ atual = null;
57
+ return `tempos · ${partes.join(' · ')}${partes.length ? ' · ' : ''}total ${fmt(total)}`;
58
+ }
59
+
60
+ function ativo() { return !!atual; }
61
+
62
+ module.exports = { ligado, comecar, somar, medir, relatorio, ativo };
package/lib/ui.js CHANGED
@@ -60,9 +60,19 @@ const c = {
60
60
  // Só glifos de bloco (U+2580–U+259F): largura 1 garantida em qualquer terminal
61
61
  // monospace, sem emoji e sem depender de fonte instalada.
62
62
  //
63
- // A família compartilha corpo e pés — o que muda é a linha de cima. É de
64
- // propósito: são o mesmo bicho de orelhas diferentes, não três desenhos de
65
- // autores diferentes. Troque com /mascot.
63
+ // Primo, raposa e gato compartilham corpo e pés — são o mesmo bicho de
64
+ // orelhas diferentes. Os que chegaram depois ("mais mascotes", pedido do
65
+ // dono) têm silhueta própria: um robô de corpo de gato não seria um robô.
66
+ //
67
+ // As cores saem SÓ do PALETTE. `full` pinta o bloco cheio, `edge` o contorno;
68
+ // a `mascara` (opcional, uma letra por coluna) marca o que foge da regra —
69
+ // olho (`o`), acento (`a`), corpo (`c`), borda (`b`). É por ela que o olho do
70
+ // robô acende na primária e a barriga do pinguim fica clara, sem inventar cor.
71
+ //
72
+ // Cada um tem nome, bordão (aparece na saudação) e um rostinho por humor, que
73
+ // sai numa linha discreta depois da resposta do agente (/mascot reacoes off
74
+ // desliga). Rosto em ASCII/Unicode de largura 1 — emoji ocupa duas colunas e
75
+ // muda de desenho de um terminal para outro.
66
76
 
67
77
  const ART_W = 9; // colunas de cada linha do desenho
68
78
  const ART_H = 3; // linhas — igual ao número de linhas de identidade
@@ -72,28 +82,168 @@ const PES = ' ▘▘ ▝▝ ';
72
82
 
73
83
  const MASCOTS = {
74
84
  // Padrão: o Primo — cabeça cheia, ombros marcados, dois pezinhos.
75
- primo: { topo: ' ▐▛███▜▌ ', full: 'brand', edge: 'brandDim' },
85
+ primo: {
86
+ nome: 'Primo', frase: 'Bora construir junto?',
87
+ linhas: [' ▐▛███▜▌ ', CORPO, PES], full: 'brand', edge: 'brandDim',
88
+ rosto: { feliz: '(^‿^)', preocupado: '(°_°)', parado: '(-_-)' },
89
+ },
76
90
  // Raposa — orelhas largas, abertas para fora. Na secundária do sistema.
77
- fox: { topo: ' ▛▀ ▀▜ ', full: 'warn', edge: 'dim' },
91
+ fox: {
92
+ nome: 'Raposa', frase: 'Esperta e rápida no código.',
93
+ linhas: [' ▛▀ ▀▜ ', CORPO, PES], full: 'warn', edge: 'dim',
94
+ rosto: { feliz: '(^ᴥ^)', preocupado: '(•ᴥ•;)', parado: '(-ᴥ-)' },
95
+ },
78
96
  // Gato — orelhas curtas e juntas. Primária recuada, para não repetir o Primo.
79
- cat: { topo: ' ▛▘ ▝▜ ', full: 'brandDim', edge: 'dim' },
97
+ cat: {
98
+ nome: 'Gato', frase: 'Curioso até achar o bug.',
99
+ linhas: [' ▛▘ ▝▜ ', CORPO, PES], full: 'brandDim', edge: 'dim',
100
+ rosto: { feliz: '(=^.^=)', preocupado: '(=°.°=)', parado: '(=-.-=)' },
101
+ },
102
+ // Robô — antena, cabeça quadrada e os dois olhos acesos na primária.
103
+ // Corpo em slate: é metal, e metal é neutro.
104
+ robo: {
105
+ nome: 'Bip', frase: 'Bip-bop: pedido entendido.',
106
+ linhas: [' ▗▄▄█▄▄▖ ', ' ▐█████▌ ', ' █▀▀▀█ '],
107
+ mascara: [' ccccc ', ' o o ', ' '],
108
+ full: 'muted', edge: 'dim', olho: 'brand',
109
+ rosto: { feliz: '[^_^]', preocupado: '[o_O]', parado: '[-_-]' },
110
+ },
111
+ // Coruja — os tufos em cima e dois olhões claros. Na secundária: é a
112
+ // cor do "aviso que não é falha", e a coruja é a que pensa antes.
113
+ coruja: {
114
+ nome: 'Sábia', frase: 'Pensa antes, erra menos.',
115
+ linhas: [' █▄▄▄▄▄█ ', ' ███▄███ ', ' ▝█▀▀▀█▘ '],
116
+ mascara: [' ccccccc ', ' oo oo ', ' '],
117
+ full: 'warn', edge: 'dim', olho: 'white',
118
+ rosto: { feliz: '(O,O)', preocupado: '(o,O)', parado: '(-,-)' },
119
+ },
120
+ // Dragão — chifres para fora, asinhas nos lados e o rabo à direita.
121
+ // Chifres na secundária, corpo na primária.
122
+ dragao: {
123
+ nome: 'Faísca', frase: 'Bota fogo no projeto!',
124
+ linhas: ['▝▙▗▄▄▄▖▟▘', '▗▟█████▙▖', ' ▝█▀▀▀█▙▖'],
125
+ mascara: ['aa aa', 'a o o a', ' '],
126
+ full: 'brand', edge: 'brandDim', olho: 'white', acento: 'warn',
127
+ rosto: { feliz: '<^w^>', preocupado: '<°w°;>', parado: '<-w->' },
128
+ },
129
+ // Fantasma — cúpula, dois olhos vazados e a barra ondulada embaixo.
130
+ fantasma: {
131
+ nome: 'Boo', frase: 'Some com os bugs sem fazer barulho.',
132
+ linhas: [' ▗▟███▙▖ ', ' ██▄█▄██ ', ' ▛▚▟▀▙▞▜ '],
133
+ mascara: [' ', ' c c ', ' '],
134
+ full: 'white', edge: 'muted',
135
+ rosto: { feliz: '(^o^)~', preocupado: '(°o°)~', parado: '(-o-)~' },
136
+ },
137
+ // Capivara — de perfil: orelhinha, focinho reto, corpo comprido e as
138
+ // perninhas curtas. A brasileira da turma, na secundária (o mais perto do
139
+ // marrom que a paleta tem).
140
+ capivara: {
141
+ nome: 'Capi', frase: 'Na calma, tudo sai.',
142
+ linhas: [' ▗▖ ', '▗█▀█▄▄▄▄▖', '▝▀█▀▀▀█▀ '],
143
+ mascara: [' bb ', ' o cccc', ' '],
144
+ full: 'warn', edge: 'dim', olho: 'dim',
145
+ rosto: { feliz: '(•ᴗ•)', preocupado: '(•_•;)', parado: '(-ᴗ-)' },
146
+ },
147
+ // Polvo — cabeça redonda, olhos vazados e os braços abertos embaixo.
148
+ polvo: {
149
+ nome: 'Polvinho', frase: 'Oito braços, várias tarefas.',
150
+ linhas: [' ▟███▙ ', ' ▐█▄█▄█▌ ', '▗▛▐▘█▝▌▜▖'],
151
+ full: 'brandDim', edge: 'brandDim',
152
+ rosto: { feliz: '~(^u^)~', preocupado: '~(°u°)~', parado: '~(-u-)~' },
153
+ },
154
+ // Pinguim — cabeça escura, bico na secundária, barriga clara e os pés.
155
+ pinguim: {
156
+ nome: 'Gelinho', frase: 'Cabeça fria, código limpo.',
157
+ linhas: [' ▟▀█▀▙ ', ' ▟█████▙ ', ' ▝▀▘ ▝▀▘ '],
158
+ mascara: [' a ', ' ooo ', ' aaa aaa '],
159
+ full: 'dim', edge: 'dim', olho: 'white', acento: 'warn',
160
+ rosto: { feliz: '<(^v^)>', preocupado: '<(°v°)>', parado: '<(-v-)>' },
161
+ },
162
+ };
163
+
164
+ // A ordem da galeria e dos números do /mascot. Os três de sempre primeiro:
165
+ // quem escolheu o gato ontem acha ele no mesmo lugar hoje.
166
+ const MASCOTES = Object.keys(MASCOTS);
167
+
168
+ // Quem digita em português escreve "raposa", "robô", "dragão".
169
+ const APELIDOS_MASCOTE = {
170
+ raposa: 'fox', gato: 'cat', robô: 'robo', robot: 'robo', dragão: 'dragao', dragon: 'dragao',
171
+ owl: 'coruja', ghost: 'fantasma', capybara: 'capivara', capi: 'capivara', octopus: 'polvo',
172
+ penguin: 'pinguim', bip: 'robo', boo: 'fantasma', sabia: 'coruja', sábia: 'coruja',
173
+ faisca: 'dragao', faísca: 'dragao', polvinho: 'polvo', gelinho: 'pinguim',
80
174
  };
81
175
 
176
+ /** Nome, apelido ou número (1-based) → id do mascote. null se não existe. */
177
+ function resolverMascote(pedido) {
178
+ const t = String(pedido == null ? '' : pedido).trim().toLowerCase();
179
+ if (!t) return null;
180
+ if (/^\d+$/.test(t)) return MASCOTES[Number(t) - 1] || null;
181
+ if (MASCOTS[t]) return t;
182
+ return APELIDOS_MASCOTE[t] || null;
183
+ }
184
+
185
+ function mascoteInfo(style) {
186
+ const id = MASCOTS[style] ? style : 'primo';
187
+ const m = MASCOTS[id];
188
+ return { id, nome: m.nome, frase: m.frase, rosto: { ...m.rosto } };
189
+ }
190
+
82
191
  // Bloco cheio na cor viva, contorno na apagada: dá volume ao desenho sem
83
- // precisar de gradiente nem de cor de fundo.
84
- function shade(line, full, edge) {
192
+ // precisar de gradiente nem de cor de fundo. A máscara ganha da regra.
193
+ const PAPEL = { c: 'full', b: 'edge', o: 'olho', a: 'acento' };
194
+ function shade(line, m, mascara) {
85
195
  let out = '';
86
- for (const ch of line) {
87
- if (ch === ' ') out += ch;
88
- else if (ch === '█' || ch === '▀') out += c[full](ch);
89
- else out += c[edge](ch);
90
- }
196
+ const letras = [...(mascara || '')];
197
+ [...line].forEach((ch, i) => {
198
+ if (ch === ' ') { out += ch; return; }
199
+ const papel = PAPEL[letras[i]];
200
+ const cor = (papel && m[papel]) || ((ch === '█' || ch === '▀') ? m.full : m.edge);
201
+ out += c[cor](ch);
202
+ });
91
203
  return out;
92
204
  }
93
205
 
94
206
  function mascot(style) {
95
207
  const m = MASCOTS[style] || MASCOTS.primo;
96
- return [m.topo, CORPO, PES].map((l) => shade(l, m.full, m.edge));
208
+ return m.linhas.map((l, i) => shade(l, m, m.mascara && m.mascara[i]));
209
+ }
210
+
211
+ /**
212
+ * A galeria do /mascot: todos lado a lado, com o número e o nome em cima.
213
+ * Célula de 13 colunas ("● 10 pinguim" + folga); quantas cabem por fileira
214
+ * sai da largura do terminal, então 80 colunas dá seis por fileira.
215
+ */
216
+ function galeriaMascotes(atual, colunas) {
217
+ const CEL = 13;
218
+ const cols = Math.max(CEL + 2, Math.min(colunas || process.stdout.columns || 80, 120));
219
+ const porFileira = Math.max(1, Math.floor((cols - 2) / CEL));
220
+ const out = [];
221
+ for (let i = 0; i < MASCOTES.length; i += porFileira) {
222
+ const grupo = MASCOTES.slice(i, i + porFileira);
223
+ const rotulo = grupo.map((id, j) => {
224
+ const txt = `${i + j + 1} ${id}`;
225
+ return id === atual ? padRight(c.brand('● ' + txt), CEL) : padRight(c.muted(' ' + txt), CEL);
226
+ }).join('');
227
+ out.push(' ' + rotulo.replace(/\s+$/, ''));
228
+ for (let l = 0; l < ART_H; l++) {
229
+ out.push((' ' + grupo.map((id) => ' ' + mascot(id)[l] + ' ').join('')).replace(/\s+$/, ''));
230
+ }
231
+ out.push('');
232
+ }
233
+ return out.join('\n');
234
+ }
235
+
236
+ /* A reação depois da resposta do agente: o rostinho e uma palavra. Uma linha,
237
+ cor apagada — é tempero, não aviso. O humor sai do que aconteceu:
238
+ terminou (feliz), parou sem concluir (preocupado), a pessoa mandou parar
239
+ (parado). Cor segue função: primária para o que deu certo, secundária para
240
+ o que pede atenção. */
241
+ const DITO = { feliz: 'pronto!', preocupado: 'não terminei tudo, dá uma olhada', parado: 'parei aqui' };
242
+ function reacao(style, humor) {
243
+ const m = MASCOTS[style] || MASCOTS.primo;
244
+ const h = m.rosto[humor] ? humor : 'feliz';
245
+ const cor = h === 'feliz' ? 'brand' : h === 'preocupado' ? 'warn' : 'muted';
246
+ return ` ${c[cor](m.rosto[h])} ${c.dim(m.nome + ': ' + DITO[h])}`;
97
247
  }
98
248
 
99
249
  // ── Medição de largura ───────────────────────────────────────────────────
@@ -177,9 +327,13 @@ function welcomeBox(version, contexto, opts = {}) {
177
327
  const art = mascot(style);
178
328
  const cols = Math.max(32, Math.min(process.stdout.columns || 100, 120));
179
329
 
180
- const saudacao = userName
330
+ // O bordão do mascote vem na mesma linha da saudação: é ele quem cumprimenta.
331
+ // Em terminal estreito o truncate corta o bordão antes do nome.
332
+ const info = mascoteInfo(style);
333
+ const saudacao = (userName
181
334
  ? `${c.muted('Bem-vindo de volta, ')}${c.bold(c.white(userName))}`
182
- : c.muted('Bem-vindo de volta');
335
+ : c.muted('Bem-vindo de volta'))
336
+ + c.dim(` · ${info.nome}: "${info.frase}"`);
183
337
 
184
338
  // As três linhas de identidade — uma por linha do mascote, na ordem em que
185
339
  // se lê: quem é, quem chegou, onde estamos.
@@ -455,4 +609,4 @@ function friendlyError(raw) {
455
609
  }
456
610
 
457
611
 
458
- module.exports = { c, mascot, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
612
+ module.exports = { c, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0-beta.5",
3
+ "version": "9.8.0-beta.7",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/kits.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/kits.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/ide.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -0,0 +1,52 @@
1
+ ---
2
+ nome: Apresentação PowerPoint
3
+ descricao: gerar um .pptx editável (PowerPoint, Keynote, Google Slides) com python-pptx, e conferir slide por slide
4
+ quando: pptx, powerpoint, power point, keynote, google slides, apresentação editável, slides editáveis
5
+ autor: PrimoCode
6
+ ---
7
+ Primeiro a pergunta certa: a pessoa precisa EDITAR depois, ou só apresentar?
8
+ Só apresentar → o Estúdio faz slides com mais liberdade visual e entrega em PDF
9
+ (`estudio_projeto` formato "slides"). Precisa abrir no PowerPoint, no Keynote ou
10
+ no Google Slides e mexer → .pptx, com `python-pptx`.
11
+
12
+ **1. Prepare.** `python3 -m venv .venv && .venv/bin/pip install python-pptx`, e um
13
+ script (`gerar_slides.py`) que dá para rodar de novo a cada ajuste.
14
+
15
+ **2. Tamanho 16:9 e layout em branco.** O padrão da biblioteca é 4:3.
16
+ ```python
17
+ from pptx import Presentation
18
+ from pptx.util import Inches, Pt
19
+ from pptx.dml.color import RGBColor
20
+ prs = Presentation()
21
+ prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
22
+ s = prs.slides.add_slide(prs.slide_layouts[6]) # 6 = em branco
23
+ caixa = s.shapes.add_textbox(Inches(0.8), Inches(0.8), Inches(11.7), Inches(1.5))
24
+ p = caixa.text_frame.paragraphs[0]
25
+ p.text = 'O custo dobrou em seis meses'
26
+ p.font.size, p.font.bold = Pt(44), True
27
+ p.font.color.rgb = RGBColor(0x1A, 0x23, 0x32)
28
+ s.notes_slide.notes_text_frame.text = 'O que falar neste slide.'
29
+ prs.save('apresentacao.pptx')
30
+ ```
31
+
32
+ **3. Regras que evitam o slide quebrado**
33
+ - O `python-pptx` NÃO encolhe texto que não cabe: ele vaza da caixa. Escreva
34
+ curto, uma ideia por slide, e dê à caixa a altura do texto.
35
+ - Corpo com pelo menos 24pt; título entre 36 e 54pt.
36
+ - Fonte que a outra máquina tem (Arial, Calibri, Georgia, Helvetica). O .pptx
37
+ não leva a fonte junto: a que falta é trocada e o texto muda de tamanho.
38
+ - Margem de 0,6 polegada em volta, e a mesma posição de título em todo slide.
39
+ - Imagem com `s.shapes.add_picture('foto.jpg', x, y, width=...)`: dê só a
40
+ largura, para ela não esticar.
41
+ - O que a pessoa vai FALAR vai nas notas, não no slide.
42
+
43
+ O conteúdo segue as regras da skill de slides: título que afirma, uma ideia por
44
+ slide, dado com fonte.
45
+
46
+ **4. Confira antes de entregar.**
47
+ - Reabra com `Presentation('apresentacao.pptx')` e imprima o número de slides e
48
+ o título de cada um.
49
+ - Com LibreOffice: `soffice --headless --convert-to pdf apresentacao.pptx`,
50
+ abra o PDF e OLHE cada página. Texto vazando da caixa, imagem cortada e slide
51
+ vazio só aparecem assim.
52
+ - Diga à pessoa que o Google Slides abre o .pptx por Arquivo → Importar.