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.
- package/README.md +15 -4
- package/app/ansi.js +55 -0
- package/app/ide.css +345 -0
- package/app/ide.html +183 -0
- package/app/ide.js +639 -0
- package/bin/primocode.js +338 -88
- package/kits/acolhedor/index.html +130 -0
- package/kits/acolhedor/script.js +25 -0
- package/kits/acolhedor/style.css +146 -0
- package/kits/editorial/index.html +115 -0
- package/kits/editorial/script.js +19 -0
- package/kits/editorial/style.css +112 -0
- package/kits/premium/index.html +83 -0
- package/kits/premium/script.js +11 -0
- package/kits/premium/style.css +87 -0
- package/kits/vitrine/index.html +108 -0
- package/kits/vitrine/script.js +20 -0
- package/kits/vitrine/style.css +117 -0
- package/lib/act.js +66 -7
- package/lib/app.js +41 -6
- package/lib/browser.js +79 -3
- package/lib/checkpoint-git.js +218 -0
- package/lib/checkpoint.js +159 -44
- package/lib/comandos-extra.js +481 -0
- package/lib/config.js +5 -2
- package/lib/ide.js +337 -0
- package/lib/kits.js +165 -54
- package/lib/pre-leitura.js +118 -0
- package/lib/repeticao.js +4 -2
- package/lib/skills.js +6 -1
- package/lib/tempos.js +62 -0
- package/lib/ui.js +171 -17
- package/package.json +2 -2
- package/skills/apresentacao-pptx/SKILL.md +52 -0
- package/skills/cartaz-de-arte/LICENSE.txt +202 -0
- package/skills/cartaz-de-arte/SKILL.md +47 -0
- package/skills/criar-skill/LICENSE.txt +202 -0
- package/skills/criar-skill/SKILL.md +49 -0
- package/skills/depurar-de-verdade/LICENSE.txt +21 -0
- package/skills/depurar-de-verdade/SKILL.md +14 -5
- package/skills/documento-word/SKILL.md +57 -0
- package/skills/interface-revisada/SKILL.md +47 -0
- package/skills/pdf-bonito/SKILL.md +48 -0
- package/skills/planilha-excel/SKILL.md +47 -0
- package/skills/react-sem-dor/SKILL.md +48 -0
- package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
- package/skills/temas-prontos/LICENSE.txt +202 -0
- package/skills/temas-prontos/SKILL.md +50 -0
- package/skills/testar-o-site/LICENSE.txt +202 -0
- package/skills/testar-o-site/SKILL.md +46 -0
- 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
|
-
|
|
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
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
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: {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|