primocode 10.5.0 → 10.6.0
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 +2 -2
- package/bin/primocode.js +117 -24
- package/lib/act.js +141 -9
- package/lib/agents-md.js +140 -0
- package/lib/auditoria.js +122 -0
- package/lib/browser.js +48 -1
- package/lib/catalogo.js +24 -4
- package/lib/comandos-extra.js +2 -0
- package/lib/config.js +32 -4
- package/lib/entrada.js +33 -11
- package/lib/estudio/colecao.js +34 -1
- package/lib/estudio/diretor.js +1 -0
- package/lib/estudio/falantes.js +30 -6
- package/lib/estudio/hibrido.js +144 -8
- package/lib/estudio/index.js +151 -59
- package/lib/estudio/kit/mundo.jsx +1060 -0
- package/lib/estudio/kit/voxel.jsx +315 -70
- package/lib/estudio/kit.js +1 -1
- package/lib/estudio/kokoro.js +18 -4
- package/lib/estudio/margem.js +1 -1
- package/lib/estudio/mundo.js +305 -0
- package/lib/estudio/personagem.js +164 -11
- package/lib/estudio/producao.js +107 -17
- package/lib/estudio/projeto.js +77 -22
- package/lib/leitura.js +176 -0
- package/lib/local/cerebro.json +180 -3
- package/lib/local/entrada.js +61 -4
- package/lib/mcp.js +296 -12
- package/lib/memoria-conversa.js +32 -3
- package/lib/mencoes.js +5 -1
- package/lib/nome-curto.js +126 -0
- package/lib/nome-projeto.js +5 -2
- package/lib/outra-pasta.js +161 -0
- package/lib/perigo.js +34 -2
- package/lib/referencia.js +40 -11
- package/lib/repeticao.js +20 -1
- package/lib/tela.js +1 -1
- package/lib/tools.js +76 -6
- package/lib/travado.js +141 -0
- package/lib/ui.js +1 -0
- package/package.json +2 -2
package/lib/agents-md.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents-md.js — a memória da conversa mora NA PASTA, num AGENTS.md.
|
|
3
|
+
*
|
|
4
|
+
* "Eu preciso de um AGENTS.md com a memória da conversa em cada pasta."
|
|
5
|
+
* (11/10/2026)
|
|
6
|
+
*
|
|
7
|
+
* O resumo da conversa (lib/memoria-conversa.js) já existia, mas vivia só na
|
|
8
|
+
* memória do processo: fechar o terminal apagava o "de onde parei". Agora,
|
|
9
|
+
* a cada resposta, ele é escrito no AGENTS.md da pasta em que se está — e a
|
|
10
|
+
* próxima sessão aberta ali lê e continua.
|
|
11
|
+
*
|
|
12
|
+
* ── POR QUE AGENTS.md, E POR QUE SÓ UM BLOCO DELE ────────────────────────
|
|
13
|
+
* AGENTS.md é o arquivo que Codex, Cursor e Claude Code já leem; a memória
|
|
14
|
+
* escrita aqui serve a qualquer agente que abrir a pasta. Mas o repo da
|
|
15
|
+
* pessoa pode JÁ TER um AGENTS.md, escrito por ela. Então o PrimoCode só
|
|
16
|
+
* escreve entre as duas marcas abaixo e nunca toca no resto: se o arquivo não
|
|
17
|
+
* existe, nasce com o bloco; se existe sem bloco, o bloco vai no fim; se tem
|
|
18
|
+
* bloco, só o miolo dele muda.
|
|
19
|
+
*
|
|
20
|
+
* ── CURTO DE PROPÓSITO ───────────────────────────────────────────────────
|
|
21
|
+
* O bloco entra no contexto de todo pedido naquela pasta. Teto de 2.400
|
|
22
|
+
* letras: o resumo já é podado por quem resume (250 palavras), e o teto
|
|
23
|
+
* aqui é a trava para o dia em que não for.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const ARQUIVO = 'AGENTS.md';
|
|
33
|
+
const INICIO = '<!-- primocode:memoria -->';
|
|
34
|
+
const FIM = '<!-- /primocode:memoria -->';
|
|
35
|
+
const TETO = 2400;
|
|
36
|
+
|
|
37
|
+
function escaparRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
38
|
+
const RE_BLOCO = new RegExp(`${escaparRe(INICIO)}[\\s\\S]*?${escaparRe(FIM)}`);
|
|
39
|
+
|
|
40
|
+
/** O miolo do bloco (sem cabeçalho nem marcas), ou ''. */
|
|
41
|
+
function extrair(texto) {
|
|
42
|
+
const m = String(texto || '').match(RE_BLOCO);
|
|
43
|
+
if (!m) return '';
|
|
44
|
+
return m[0].slice(INICIO.length, -FIM.length)
|
|
45
|
+
.replace(/^\s*## Memória do PrimoCode\s*\n/, '')
|
|
46
|
+
.replace(/^\s*>.*\n/, '')
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** O texto da PESSOA: o arquivo sem o bloco da memória. */
|
|
51
|
+
function semBloco(texto) {
|
|
52
|
+
const resto = String(texto || '').replace(RE_BLOCO, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
53
|
+
// Só o título que o próprio PrimoCode pôs ao criar o arquivo: não é
|
|
54
|
+
// instrução de ninguém, e não pode tapar o CLAUDE.md do repo.
|
|
55
|
+
return /^#[^\n]*$/.test(resto) ? '' : resto;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Corta no fim de uma linha, nunca no meio: o resumo vem em tópicos, e meio
|
|
60
|
+
* tópico é pior que tópico nenhum.
|
|
61
|
+
*/
|
|
62
|
+
function encurtar(corpo, teto = TETO) {
|
|
63
|
+
const t = String(corpo || '').trim();
|
|
64
|
+
if (t.length <= teto) return t;
|
|
65
|
+
const linhas = t.split('\n');
|
|
66
|
+
let saida = '';
|
|
67
|
+
for (const l of linhas) {
|
|
68
|
+
if ((saida + l).length + 1 > teto - 12) break;
|
|
69
|
+
saida += (saida ? '\n' : '') + l;
|
|
70
|
+
}
|
|
71
|
+
return (saida || t.slice(0, teto - 12)) + '\n(… cortado)';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function quandoLocal(d = new Date()) {
|
|
75
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
76
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function bloco(corpo, quando) {
|
|
80
|
+
return [
|
|
81
|
+
INICIO,
|
|
82
|
+
'## Memória do PrimoCode',
|
|
83
|
+
`> Escrita pelo PrimoCode a cada resposta (${quando}). Ele lê isto ao abrir esta pasta e continua daqui. Fora deste bloco ele nunca mexe.`,
|
|
84
|
+
'',
|
|
85
|
+
encurtar(corpo),
|
|
86
|
+
FIM,
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* O arquivo novo, a partir do que existe. Puro: é aqui que mora a promessa
|
|
92
|
+
* de não estragar o que a pessoa escreveu, e o teste mede sem disco.
|
|
93
|
+
*/
|
|
94
|
+
function aplicar(existente, corpo, { titulo = 'Projeto', quando = quandoLocal() } = {}) {
|
|
95
|
+
const novo = bloco(corpo, quando);
|
|
96
|
+
if (existente == null || !String(existente).trim()) return `# ${titulo}\n\n${novo}\n`;
|
|
97
|
+
const texto = String(existente);
|
|
98
|
+
// Função no replace: um "$&" dentro do resumo não pode virar padrão.
|
|
99
|
+
if (RE_BLOCO.test(texto)) return texto.replace(RE_BLOCO, () => novo);
|
|
100
|
+
return `${texto.replace(/\s+$/, '')}\n\n${novo}\n`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Pasta onde vale escrever. A home e a raiz do disco não: um AGENTS.md
|
|
105
|
+
* solto em ~ seria lido por todo agente de todo projeto. A raiz de trabalho
|
|
106
|
+
* (~/primocode) também não — é o corredor, não um projeto, e o dono já
|
|
107
|
+
* reclamou dos arquivos soltos ali.
|
|
108
|
+
*/
|
|
109
|
+
function podeGravar(dir, { raiz } = {}) {
|
|
110
|
+
if (!dir) return false;
|
|
111
|
+
const alvo = path.resolve(dir);
|
|
112
|
+
if (alvo === path.resolve(os.homedir())) return false;
|
|
113
|
+
if (alvo === path.parse(alvo).root) return false;
|
|
114
|
+
if (raiz && alvo === path.resolve(raiz)) return false;
|
|
115
|
+
try { return fs.statSync(alvo).isDirectory(); } catch { return false; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A memória gravada nesta pasta, ou ''. */
|
|
119
|
+
function ler(dir) {
|
|
120
|
+
if (!dir) return '';
|
|
121
|
+
try { return extrair(fs.readFileSync(path.join(dir, ARQUIVO), 'utf-8')); } catch { return ''; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Grava (ou atualiza) o bloco. Devolve true quando escreveu. Não reescreve
|
|
126
|
+
* se o miolo não mudou: o arquivo é do repo da pessoa, e mexer na data à toa
|
|
127
|
+
* suja o `git status` dela a cada resposta.
|
|
128
|
+
*/
|
|
129
|
+
function gravar(dir, corpo, opts = {}) {
|
|
130
|
+
const limpo = String(corpo || '').trim();
|
|
131
|
+
if (!limpo || !podeGravar(dir, opts)) return false;
|
|
132
|
+
const arq = path.join(dir, ARQUIVO);
|
|
133
|
+
let existente = null;
|
|
134
|
+
try { existente = fs.readFileSync(arq, 'utf-8'); } catch { existente = null; }
|
|
135
|
+
if (existente != null && extrair(existente) === encurtar(limpo)) return false;
|
|
136
|
+
const novo = aplicar(existente, limpo, { titulo: opts.titulo || path.basename(path.resolve(dir)), quando: opts.quando });
|
|
137
|
+
try { fs.writeFileSync(arq, novo, 'utf-8'); return true; } catch { return false; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { ARQUIVO, INICIO, FIM, TETO, extrair, semBloco, encurtar, aplicar, podeGravar, ler, gravar };
|
package/lib/auditoria.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auditoria.js — anota ANTES de agir (o "decide, registra, age" do OpenBot).
|
|
3
|
+
*
|
|
4
|
+
* O registro.js faz o balanço do pedido na tela, e some quando a tela fecha.
|
|
5
|
+
* Este é o livro que fica: ~/.primocode/registro/acoes-AAAA-MM-DD.jsonl, uma
|
|
6
|
+
* linha por decisão, escrita ANTES de a ferramenta rodar — então o que foi
|
|
7
|
+
* recusado (perigo, permissão, freio) e o que travou no meio também aparece.
|
|
8
|
+
* Depois vem uma segunda linha com o resultado, ligada pelo `id`.
|
|
9
|
+
*
|
|
10
|
+
* Nunca guarda conteúdo de arquivo nem ambiente: argumento longo vira
|
|
11
|
+
* "<N caracteres>", chave com cara de segredo vira "***". Um arquivo por dia,
|
|
12
|
+
* com teto de tamanho, e os de mais de RETER_DIAS dias são apagados.
|
|
13
|
+
* Falhar aqui nunca derruba o agente — mas também nunca some calado: passou
|
|
14
|
+
* do teto, abre o arquivo seguinte do dia (acoes-AAAA-MM-DD.2.jsonl); não deu
|
|
15
|
+
* para gravar, `problema()` devolve o porquê, uma vez, e o act.js mostra na
|
|
16
|
+
* tela. Antes, o livro cheio parava de gravar em silêncio, e quem enchesse o
|
|
17
|
+
* dia de propósito cegava o resto dele (review do #95, item 3).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
27
|
+
const RETER_DIAS = 14;
|
|
28
|
+
const LONGOS = /^(content|new_string|old_string|edits|texto|text|body|html|script)$/i;
|
|
29
|
+
const SEGREDO_CHAVE = /(token|senha|password|secret|api[_-]?key|auth|cookie|credencial)/i;
|
|
30
|
+
|
|
31
|
+
let seq = 0;
|
|
32
|
+
let limpou = false;
|
|
33
|
+
let problemaNovo = null;
|
|
34
|
+
const problemasMostrados = new Set();
|
|
35
|
+
|
|
36
|
+
const pasta = () => path.join(os.homedir(), '.primocode', 'registro');
|
|
37
|
+
function arquivoDoDia(d = new Date(), parte = 1) {
|
|
38
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
39
|
+
const sufixo = parte > 1 ? `.${parte}` : '';
|
|
40
|
+
return path.join(pasta(), `acoes-${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}${sufixo}.jsonl`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** O arquivo do dia que ainda cabe: o primeiro abaixo do teto. */
|
|
44
|
+
function arquivoComEspaco(d = new Date()) {
|
|
45
|
+
for (let parte = 1; ; parte++) {
|
|
46
|
+
const arq = arquivoDoDia(d, parte);
|
|
47
|
+
let tamanho = 0;
|
|
48
|
+
try { tamanho = fs.statSync(arq).size; } catch { return arq; }
|
|
49
|
+
if (tamanho <= MAX_BYTES) return arq;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function semSegredo(texto) {
|
|
54
|
+
return String(texto)
|
|
55
|
+
.replace(/\b(sk|ghp|gho|ghs|xox[abp]|pk|rk)[-_][A-Za-z0-9_-]{8,}/g, '***')
|
|
56
|
+
.replace(/((?:token|senha|password|secret|api[_-]?key)\s*[=:]\s*)\S+/gi, '$1***')
|
|
57
|
+
.replace(/(authorization:\s*bearer\s+)\S+/gi, '$1***');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** O resumo dos argumentos: curto, sem conteúdo e sem segredo. */
|
|
61
|
+
function resumirArgs(args) {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [k, v] of Object.entries(args || {})) {
|
|
64
|
+
if (SEGREDO_CHAVE.test(k)) out[k] = '***';
|
|
65
|
+
else if (LONGOS.test(k)) out[k] = `<${JSON.stringify(v == null ? '' : v).length} caracteres>`;
|
|
66
|
+
else if (typeof v === 'string') out[k] = semSegredo(v.length > 160 ? v.slice(0, 160) + '…' : v);
|
|
67
|
+
else if (v && typeof v === 'object') out[k] = semSegredo(JSON.stringify(v).slice(0, 160));
|
|
68
|
+
else out[k] = v;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function limparVelhos() {
|
|
74
|
+
if (limpou) return;
|
|
75
|
+
limpou = true;
|
|
76
|
+
const limite = Date.now() - RETER_DIAS * 86400000;
|
|
77
|
+
for (const nome of fs.readdirSync(pasta())) {
|
|
78
|
+
const m = /^acoes-(\d{4}-\d{2}-\d{2})(?:\.\d+)?\.jsonl$/.exec(nome);
|
|
79
|
+
if (m && Date.parse(m[1]) < limite) fs.unlinkSync(path.join(pasta(), nome));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function escrever(linha) {
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(pasta(), { recursive: true });
|
|
86
|
+
limparVelhos();
|
|
87
|
+
fs.appendFileSync(arquivoComEspaco(), JSON.stringify(linha) + '\n');
|
|
88
|
+
return true;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// O livro nunca derruba o agente, mas a falha aparece (problema()).
|
|
91
|
+
const motivo = `o livro de ações não está gravando (${(e && e.code) || (e && e.message) || e})`;
|
|
92
|
+
if (!problemasMostrados.has(motivo)) problemaNovo = motivo;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A falha de gravação ainda não mostrada, uma vez; depois, null. */
|
|
98
|
+
function problema() {
|
|
99
|
+
const p = problemaNovo;
|
|
100
|
+
problemaNovo = null;
|
|
101
|
+
if (p) problemasMostrados.add(p);
|
|
102
|
+
return p;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A linha de ANTES. Devolve o id para ligar o resultado. */
|
|
106
|
+
function antes({ agente, ferramenta, args, decisao, motivo }) {
|
|
107
|
+
const id = `${process.pid}-${Date.now().toString(36)}-${++seq}`;
|
|
108
|
+
const linha = { id, quando: new Date().toISOString(), agente, ferramenta, args: resumirArgs(args), decisao };
|
|
109
|
+
if (motivo) linha.motivo = semSegredo(String(motivo).slice(0, 200));
|
|
110
|
+
escrever(linha);
|
|
111
|
+
return id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A linha de DEPOIS: deu certo ou não, e o erro curto. */
|
|
115
|
+
function depois(id, result) {
|
|
116
|
+
const ok = !(result && result.ok === false);
|
|
117
|
+
const linha = { id, quando: new Date().toISOString(), resultado: ok ? 'ok' : 'falhou' };
|
|
118
|
+
if (!ok && result.error) linha.erro = semSegredo(String(result.error).slice(0, 200));
|
|
119
|
+
escrever(linha);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { antes, depois, problema, resumirArgs, arquivoDoDia, arquivoComEspaco, MAX_BYTES, RETER_DIAS };
|
package/lib/browser.js
CHANGED
|
@@ -1164,6 +1164,12 @@ async function abaEscondida(url, op = {}) {
|
|
|
1164
1164
|
await s.send('Page.enable');
|
|
1165
1165
|
await s.send('Runtime.enable');
|
|
1166
1166
|
await s.send('Log.enable');
|
|
1167
|
+
// A leitura em cascata pede isto: o Chrome segue redirect e o
|
|
1168
|
+
// JavaScript da página pede a rede sozinho. A mesma trava da busca
|
|
1169
|
+
// simples (hostBloqueado) vale para cada pedido, não só para a URL
|
|
1170
|
+
// inicial. Sem isto, uma página pública abre e o passo 2 alcança a
|
|
1171
|
+
// rede local (review do #96).
|
|
1172
|
+
if (op.bloquearPrivado) await travarRedeLocal(s);
|
|
1167
1173
|
// A aba nova nasce ATRÁS do about:blank inicial, e aba de fundo é
|
|
1168
1174
|
// onde o Chrome economiza: para o vídeo, estrangula os timers. Trazer
|
|
1169
1175
|
// para a frente é o que faz a página se comportar como na tela da pessoa.
|
|
@@ -1232,6 +1238,47 @@ async function abrirEscondido(url) {
|
|
|
1232
1238
|
return { fechar: aba.fechar, erros: aba.erros };
|
|
1233
1239
|
}
|
|
1234
1240
|
|
|
1241
|
+
/* Rede local, link-local e arquivo: a mesma lista da busca simples. Exigir
|
|
1242
|
+
aqui, e não só em referencia.js, porque o Chrome é quem abre a URL. */
|
|
1243
|
+
function destinoDeRedeLocal(url) {
|
|
1244
|
+
let u;
|
|
1245
|
+
try { u = new URL(url); } catch { return true; }
|
|
1246
|
+
if (u.protocol === 'file:') return true;
|
|
1247
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
|
|
1248
|
+
return require('./referencia').hostBloqueado(u.hostname);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
async function travarRedeLocal(s) {
|
|
1252
|
+
const anterior = s.onEvent;
|
|
1253
|
+
s.onEvent = (metodo, p) => {
|
|
1254
|
+
if (metodo === 'Fetch.requestPaused') {
|
|
1255
|
+
const bloquear = destinoDeRedeLocal(p.request && p.request.url);
|
|
1256
|
+
const chamada = bloquear ? 'Fetch.failRequest' : 'Fetch.continueRequest';
|
|
1257
|
+
const params = bloquear
|
|
1258
|
+
? { requestId: p.requestId, errorReason: 'BlockedByClient' }
|
|
1259
|
+
: { requestId: p.requestId };
|
|
1260
|
+
s.send(chamada, params).catch(() => {});
|
|
1261
|
+
}
|
|
1262
|
+
if (anterior) anterior(metodo, p);
|
|
1263
|
+
};
|
|
1264
|
+
await s.send('Fetch.enable', { patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/* O HTML que a página tem DEPOIS de rodar o JavaScript: o passo 2 da leitura
|
|
1268
|
+
em cascata (lib/leitura.js), pra página que a busca simples devolve vazia. */
|
|
1269
|
+
async function htmlEscondido(url, op = {}) {
|
|
1270
|
+
if (destinoDeRedeLocal(url)) return null;
|
|
1271
|
+
const aba = await abaEscondida(url, { prazoLoad: op.prazo || 12000, assentar: op.assentar || 1500, bloquearPrivado: true });
|
|
1272
|
+
try {
|
|
1273
|
+
const r = await aba.sessao.send('Runtime.evaluate', {
|
|
1274
|
+
expression: 'document.documentElement.outerHTML', returnByValue: true,
|
|
1275
|
+
}).catch(() => null);
|
|
1276
|
+
return (r && r.result && r.result.value) || null;
|
|
1277
|
+
} finally {
|
|
1278
|
+
await aba.fechar();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1235
1282
|
/* O revisor é ferramenta de uma tarefa, não um processo que fica. Deixá-lo de
|
|
1236
1283
|
pé come memória até a pessoa desligar o computador, e ele não é o navegador
|
|
1237
1284
|
dela — ninguém sentiria falta nem saberia que existe para fechar. */
|
|
@@ -1296,5 +1343,5 @@ module.exports = {
|
|
|
1296
1343
|
scriptDaSeta, verSeta, findChrome,
|
|
1297
1344
|
open, click, clickText, clickSelector, type, typeInSelector,
|
|
1298
1345
|
key, screenshot, evaluate, waitFor, wait, scroll, close,
|
|
1299
|
-
inspecionarEscondido, rodarEscondido, abrirEscondido, abaEscondida, fecharRevisor, subirRevisor, aquecerRevisor,
|
|
1346
|
+
inspecionarEscondido, rodarEscondido, abrirEscondido, abaEscondida, htmlEscondido, fecharRevisor, subirRevisor, aquecerRevisor,
|
|
1300
1347
|
};
|
package/lib/catalogo.js
CHANGED
|
@@ -31,9 +31,9 @@ const GRUPOS = [
|
|
|
31
31
|
{ tool: 'list_dir', escreve: false, faz: 'mostra a árvore de pastas',
|
|
32
32
|
args: 'path (opcional, padrão "."). depth (opcional, número, padrão 2).' },
|
|
33
33
|
{ tool: 'glob', escreve: false, faz: 'acha arquivo pelo nome (**/*.js)',
|
|
34
|
-
args: 'pattern: padrão de nome, ex "**/*.js" ou "src/**/*.{ts,tsx}".' },
|
|
34
|
+
args: 'pattern: padrão de nome, ex "**/*.js" ou "src/**/*.{ts,tsx}". Numa pasta que o usuário citou: padrão absoluto ("/caminho/**/*.js"), sem trocar a de trabalho.' },
|
|
35
35
|
{ tool: 'grep', escreve: false, faz: 'acha texto dentro dos arquivos',
|
|
36
|
-
args: 'pattern: expressão regular. glob (opcional): restringe aos arquivos que casam. ignoreCase (opcional, bool). contexto (opcional, número): linhas ao redor do achado.' },
|
|
36
|
+
args: 'pattern: expressão regular. glob (opcional): restringe aos arquivos que casam. ignoreCase (opcional, bool). contexto (opcional, número): linhas ao redor do achado. Numa pasta que o usuário citou: glob absoluto ("/caminho/**"), sem trocar a de trabalho.' },
|
|
37
37
|
],
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -93,8 +93,8 @@ const GRUPOS = [
|
|
|
93
93
|
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
|
|
94
94
|
+ 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). O texto da TELA sai da FALA: o título usa palavras que a fala diz, cedo na frase (a tela entra quando a voz diz; palavra que a voz não diz deixa a tela esperando). '
|
|
95
95
|
+ 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
|
|
96
|
-
+ 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — SÓ quando a pessoa pedir; vazio = sem moldura (ela entrega que o vídeo foi gerado); heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone 3D RENDERIZADO da biblioteca ("fluent3d:hot beverage", "fluent3d:money bag"; procure com estudio_midia acao:icones3d — é o que mais parece 3D de verdade), uma animação pronta ("noto:1f525"; acao:animacoes), um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
97
|
-
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (a ANIMAÇÃO 3D de verdade, o vídeo GRAVADO em 3D
|
|
96
|
+
+ 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega — a COISA que a frase nomeia, 2 a 4 palavras, não a frase inteira; rota:"tipografia" em número/citação, "produto" quando a frase fala do produto; ancora:"palavra" — o clipe troca 0,2 s depois dela) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — SÓ quando a pessoa pedir; vazio = sem moldura (ela entrega que o vídeo foi gerado); heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone 3D RENDERIZADO da biblioteca ("fluent3d:hot beverage", "fluent3d:money bag"; procure com estudio_midia acao:icones3d — é o que mais parece 3D de verdade), uma animação pronta ("noto:1f525"; acao:animacoes), um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
97
|
+
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (a ANIMAÇÃO 3D de verdade, o vídeo GRAVADO em 3D no visual de blocos/voxel de render de cinema — é a escolha para animação 3D, personagem, bicho, boneco, avatar, mascote, podcast, entrevista ou alguém FALANDO em 3D; cada parte é UMA fala {falante, texto, emocao?, destaque?, acao?, cenario?}, sem tipo). NADA É FIXO, monte o que a pessoa pediu: cena:"texto livre do lugar" (praia, cidade, floresta, parque, fazenda, neve, deserto, espaço, fundo do mar, escritório, sala, cozinha, escola, palco… + hora: "à noite", "ao pôr do sol") ou cena:{lugar, hora, elementos:["palmeira","barraca","violão",…]} — elemento fora do catálogo vira ícone 3D em blocos; parte.cenario muda o lugar a partir daquela fala. Até 4 falantes; personagens:{"nome":{tipo:pessoa|robo|animal|coisa, especie (animal: cachorro|gato|cavalo|vaca|porco|ovelha|raposa|lobo|urso|panda|coelho|leao|tigre|elefante|girafa|tartaruga|galinha|pato|passaro|papagaio|coruja|pinguim|peixe|tubarao|golfinho|dinossauro|sapo|macaco), icone (coisa: "pizza", "café", "polvo" — qualquer objeto/bicho que fala), pessoa: pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba; robo: cor}} — sem tipo, o nome decide (nome de bicho vira bicho, de IA vira o mascote laranja, de objeto vira coisa); figurantes:["cachorro","pessoa"] (quem está na cena sem falar); BICHO NÃO FALA: a fala de um bicho sai da narração e ele fica na cena — só fala se a pessoa pedir ("cachorro falante") ou personagens.x.fala:true; a voz de cada falante sai do GÊNERO do nome (personagens.x.genero:f|m manda), uma voz por personagem do começo ao fim; bigode:true e cabeca:"cubo" (o rosto de pixel antigo) em personagens; sem lugar no pedido, o set é a cobertura à noite (janela da cidade, cartões de interface na frente). acao em cada parte: acenar|apontar|pular|dancar|comemorar|pensar|andar|correr. emocao feliz|serio|surpreso|pensativo|animado|curioso; destaque:"texto curto"; paineis:["título",…] só no set de telas (o padrão quando o pedido não diz lugar); visual:"liso" só se pedirem o boneco liso antigo). '
|
|
98
98
|
+ 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
|
|
99
99
|
+ 'voz (thalita|antonio|francisca). DUAS VOZES (o pedido fala em dois narradores, diálogo, conversa, entrevista): roteiro.vozes {"nome":"antonio","outro":"thalita"} e falante:"nome" em cada parte — cada um sai na sua voz. Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
|
100
100
|
+ 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '
|
|
@@ -171,6 +171,20 @@ const GRUPOS = [
|
|
|
171
171
|
+ 'A pauta é para LER e compor com as primitivas — não copie os campos dela para dentro do plano.' },
|
|
172
172
|
],
|
|
173
173
|
},
|
|
174
|
+
{
|
|
175
|
+
/* MCP primeiro, mouse por último: o serviço pela API dele (Notion,
|
|
176
|
+
GitHub, Slack…) é uma chamada; o mesmo pela tela são vinte cliques. */
|
|
177
|
+
nome: 'Conectores',
|
|
178
|
+
resumo: 'MCP: age num serviço ou app pela API dele, sem mouse — procura, conecta e usa',
|
|
179
|
+
itens: [
|
|
180
|
+
{ tool: 'mcp_procurar', escreve: false, faz: 'acha o servidor MCP de um serviço (lista curada + registro oficial)',
|
|
181
|
+
args: 'busca: o serviço ou app, ex "notion", "github", "planilha".' },
|
|
182
|
+
{ tool: 'mcp_conectar', escreve: false, faz: 'instala, sobe e valida um servidor MCP — pede autorização antes',
|
|
183
|
+
args: 'nome: curto, ex "notion" (curado: só o nome basta). comando (opcional) e args (opcional, lista): do "instalar" do mcp_procurar. env (opcional, objeto): as chaves que o servidor pede — sem elas, pergunte ao usuário.' },
|
|
184
|
+
{ tool: 'mcp_usar', escreve: false, faz: 'chama uma ferramenta de um servidor MCP conectado',
|
|
185
|
+
args: 'servidor: nome do servidor. ferramenta: nome da tool dele. argumentos (opcional, objeto): os args da tool.' },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
174
188
|
{
|
|
175
189
|
nome: 'Computador',
|
|
176
190
|
resumo: 'janelas, botões, mouse e teclado — age em qualquer programa e na área de trabalho',
|
|
@@ -214,6 +228,12 @@ const GRUPOS = [
|
|
|
214
228
|
// ser para os outros casos). Esta tool é a única porta pra lá.
|
|
215
229
|
{ tool: 'remember_general', escreve: false, faz: 'grava algo sobre o usuário que vale para todo projeto (memory-geral.md)',
|
|
216
230
|
args: 'fato: o que lembrar, em uma frase.' },
|
|
231
|
+
// A troca de pasta pede o SIM do usuário, e quem pergunta é o bin:
|
|
232
|
+
// por isso o loop (lib/act.js) atende, não o tools.execute.
|
|
233
|
+
{ tool: 'mudar_pasta', escreve: false, noLoop: true,
|
|
234
|
+
faz: 'troca a pasta de trabalho para outra que o usuário citou — só depois do sim dele',
|
|
235
|
+
args: 'caminho: a pasta (absoluto, ~/…, ou o nome de um projeto). motivo: uma frase do porquê, aparece na pergunta ao usuário. '
|
|
236
|
+
+ 'Para só LER lá não precisa trocar: read_file/list_dir com o caminho absoluto, glob/grep com padrão absoluto.' },
|
|
217
237
|
// Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
|
|
218
238
|
{ tool: 'spawn_agent', escreve: false, faz: 'chama um subagente especialista para uma parte da tarefa', noLoop: true,
|
|
219
239
|
args: 'description: 3 a 5 palavras, aparece na tela do usuário. prompt: instrução completa e autossuficiente — o subagente não vê esta conversa. especialista (opcional): frontend, backend, testes, design, dados, revisor ou ambiente — cada um chega com o briefing da profissão (lib/especialistas.js); sem ele, a tarefa sugere.' },
|
package/lib/comandos-extra.js
CHANGED
|
@@ -466,6 +466,8 @@ const ATALHOS = [
|
|
|
466
466
|
['Ctrl+C', 'limpa o que foi digitado; com a caixa vazia, sai'],
|
|
467
467
|
['Ctrl+D', 'sai (com a caixa vazia)'],
|
|
468
468
|
['Ctrl+L', 'limpa a tela'],
|
|
469
|
+
['PgUp · PgDn', 'rola a conversa'],
|
|
470
|
+
['arrastar o mouse', 'seleciona texto; Cmd+C (Mac) ou Ctrl+Shift+C copia'],
|
|
469
471
|
['Ctrl+A · Ctrl+E', 'vai para o começo · o fim da linha'],
|
|
470
472
|
['Ctrl+U · Ctrl+K', 'apaga antes · depois do cursor'],
|
|
471
473
|
['Ctrl+W', 'apaga a palavra antes do cursor'],
|
package/lib/config.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
|
+
const { nomeCurto } = require('./nome-curto.js');
|
|
16
17
|
|
|
17
18
|
const HOME = os.homedir();
|
|
18
19
|
const ROOT_DIR = path.join(HOME, 'primocode');
|
|
@@ -106,12 +107,27 @@ function slugify(name) {
|
|
|
106
107
|
.replace(/-+$/, ''); // o corte pode deixar hífen no fim
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/* O nome da pasta de um projeto em ~/primocode.
|
|
111
|
+
*
|
|
112
|
+
* Curto e legível (lib/nome-curto.js): "landing da padaria do zé com
|
|
113
|
+
* cardápio" vira `landing-padaria-ze-cardapio`, não a frase inteira. Mas uma
|
|
114
|
+
* pasta que JÁ existe com o nome comprido de antes continua sendo ela —
|
|
115
|
+
* renomear sozinho o projeto de alguém é pior que um nome feio. */
|
|
116
|
+
function nomeDaPasta(projectName) {
|
|
117
|
+
const antigo = slugify(projectName);
|
|
118
|
+
if (antigo && fs.existsSync(path.join(ROOT_DIR, antigo))) return antigo;
|
|
119
|
+
// Já veio como nome de pasta (do modelo, do /novo, com o -2 da colisão):
|
|
120
|
+
// passar de novo pela régua tiraria o "-2" e cairia no projeto do vizinho.
|
|
121
|
+
if (/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(String(projectName || '')) && antigo.length <= 40) return antigo;
|
|
122
|
+
return nomeCurto(projectName) || antigo;
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
function createProjectDir(projectName) {
|
|
110
126
|
ensureStructure();
|
|
111
127
|
/* Nome sem letra nem número ("!!!", só emoji, só japonês) dava slug
|
|
112
128
|
VAZIO — e path.join(ROOT_DIR, '') é a própria raiz: o "projeto" virava
|
|
113
129
|
~/primocode, com memory.md gravado nela. */
|
|
114
|
-
const slug =
|
|
130
|
+
const slug = nomeDaPasta(projectName) || `projeto-${today()}`;
|
|
115
131
|
const projectDir = path.join(ROOT_DIR, slug);
|
|
116
132
|
ensureDir(projectDir);
|
|
117
133
|
const memoryPath = path.join(projectDir, 'memory.md');
|
|
@@ -123,7 +139,7 @@ function createProjectDir(projectName) {
|
|
|
123
139
|
|
|
124
140
|
function getProjectDir(projectName) {
|
|
125
141
|
if (!projectName) return ROOT_DIR;
|
|
126
|
-
return path.join(ROOT_DIR, slugify(projectName));
|
|
142
|
+
return path.join(ROOT_DIR, nomeDaPasta(projectName) || slugify(projectName));
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
function listProjects() {
|
|
@@ -348,13 +364,25 @@ function appendHistory(entry) {
|
|
|
348
364
|
* ativo fica guardado aqui, num arquivo só dele (a config tem lista fechada de
|
|
349
365
|
* campos), e a próxima sessão continua nele. Vale por uma semana: depois
|
|
350
366
|
* disso, abrir o PrimoCode e cair num projeto esquecido seria a surpresa. */
|
|
367
|
+
/* As RECENTES vão junto, no mesmo arquivo: são as pastas em que a pessoa
|
|
368
|
+
trabalhou, e é por elas que "olha lá no repo do site" acha o site sem a
|
|
369
|
+
pessoa digitar o caminho (lib/outra-pasta.js). Dez bastam. */
|
|
351
370
|
function lembrarProjeto(dir, origem = 'projeto') {
|
|
352
371
|
try {
|
|
353
372
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
354
|
-
|
|
373
|
+
const recentes = [dir, ...pastasRecentes().filter((d) => path.resolve(d) !== path.resolve(dir))].slice(0, 10);
|
|
374
|
+
fs.writeFileSync(PROJETO_ATIVO_FILE, JSON.stringify({ dir, origem, quando: Date.now(), recentes }, null, 2), 'utf-8');
|
|
355
375
|
} catch { /* lembrar é conveniência: falhou, a sessão segue igual */ }
|
|
356
376
|
}
|
|
357
377
|
|
|
378
|
+
function pastasRecentes() {
|
|
379
|
+
try {
|
|
380
|
+
const j = JSON.parse(fs.readFileSync(PROJETO_ATIVO_FILE, 'utf-8'));
|
|
381
|
+
const lista = Array.isArray(j.recentes) ? j.recentes : (j.dir ? [j.dir] : []);
|
|
382
|
+
return lista.filter((d) => typeof d === 'string' && d);
|
|
383
|
+
} catch { return []; }
|
|
384
|
+
}
|
|
385
|
+
|
|
358
386
|
function projetoLembrado(maxDias = 7) {
|
|
359
387
|
try {
|
|
360
388
|
const j = JSON.parse(fs.readFileSync(PROJETO_ATIVO_FILE, 'utf-8'));
|
|
@@ -370,7 +398,7 @@ function esquecerProjeto() {
|
|
|
370
398
|
|
|
371
399
|
module.exports = {
|
|
372
400
|
tokensDoAssunto, GENERICAS,
|
|
373
|
-
lembrarProjeto, projetoLembrado, esquecerProjeto, PROJETO_ATIVO_FILE,
|
|
401
|
+
lembrarProjeto, projetoLembrado, esquecerProjeto, PROJETO_ATIVO_FILE, pastasRecentes, nomeDaPasta,
|
|
374
402
|
loadConfig, saveConfig, appendHistory, servidor, SERVIDOR,
|
|
375
403
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
376
404
|
ensureStructure, slugify, stripAccents,
|
package/lib/entrada.js
CHANGED
|
@@ -311,7 +311,7 @@ class Entrada extends EventEmitter {
|
|
|
311
311
|
this._aoTeclar = (ch, key) => this._tecla(ch, key);
|
|
312
312
|
process.stdin.on('keypress', this._aoTeclar);
|
|
313
313
|
|
|
314
|
-
this._aoRedimensionar = () => { if (this.cheia) this._pintar(); else if (this.desenhado) this._redesenhar(); };
|
|
314
|
+
this._aoRedimensionar = () => { if (this.cheia) { this._quadroAnterior = null; this._pintar(); } else if (this.desenhado) this._redesenhar(); };
|
|
315
315
|
process.stdout.on('resize', this._aoRedimensionar);
|
|
316
316
|
|
|
317
317
|
/* Todo `console.log` do programa passa a apagar a caixa antes de
|
|
@@ -399,7 +399,7 @@ class Entrada extends EventEmitter {
|
|
|
399
399
|
suspender() { this.suspenso = true; if (this.cheia) this._agendar(); else this._apagar(); }
|
|
400
400
|
retomar() {
|
|
401
401
|
this.suspenso = false;
|
|
402
|
-
if (this.cheia && this.ativo && !this.fechado) { this.painelTexto = null; return this._pintar(); }
|
|
402
|
+
if (this.cheia && this.ativo && !this.fechado) { this.painelTexto = null; this._quadroAnterior = null; return this._pintar(); }
|
|
403
403
|
if (this.ativo && !this.fechado) this._desenhar();
|
|
404
404
|
}
|
|
405
405
|
|
|
@@ -412,8 +412,13 @@ class Entrada extends EventEmitter {
|
|
|
412
412
|
// ── a tela cheia ─────────────────────────────────────────────────────
|
|
413
413
|
|
|
414
414
|
/* A tela alternativa (a do vim): o que estava no terminal fica guardado e
|
|
415
|
-
volta inteiro na saída.
|
|
416
|
-
|
|
415
|
+
volta inteiro na saída.
|
|
416
|
+
|
|
417
|
+
SEM capturar o mouse (o modo 1000 do xterm). Capturar dava a roda para rolar a
|
|
418
|
+
conversa, mas tirava do terminal o clique-e-arrasta: ninguém conseguia
|
|
419
|
+
selecionar um trecho da resposta para copiar ("na CLI não consigo
|
|
420
|
+
selecionar texto", 11/10). Selecionar vale mais que a roda: rolar fica
|
|
421
|
+
no PgUp/PgDn, e o /copiar copia a resposta inteira. */
|
|
417
422
|
_entrarNaTela() {
|
|
418
423
|
if (this._naTela) return;
|
|
419
424
|
this._naTela = true;
|
|
@@ -430,10 +435,11 @@ class Entrada extends EventEmitter {
|
|
|
430
435
|
set(v) { eu._colunasReais = v; }, // o Node escreve aqui quando a janela muda
|
|
431
436
|
});
|
|
432
437
|
}
|
|
433
|
-
this.escreverCru('\x1b[?1049h\x1b[
|
|
438
|
+
this.escreverCru('\x1b[?1049h\x1b[2J');
|
|
439
|
+
this._quadroAnterior = null; // a tela foi limpa: o próximo quadro vai inteiro
|
|
434
440
|
if (!this._aoSairDoProcesso) {
|
|
435
441
|
// Um erro que derruba o programa não pode deixar o terminal na
|
|
436
|
-
// tela alternativa
|
|
442
|
+
// tela alternativa e sem cursor.
|
|
437
443
|
this._aoSairDoProcesso = () => this._sairDaTela();
|
|
438
444
|
process.on('exit', this._aoSairDoProcesso);
|
|
439
445
|
}
|
|
@@ -442,7 +448,9 @@ class Entrada extends EventEmitter {
|
|
|
442
448
|
_sairDaTela() {
|
|
443
449
|
if (!this._naTela) return;
|
|
444
450
|
this._naTela = false;
|
|
445
|
-
|
|
451
|
+
/* Desliga o mouse mesmo sem ter ligado: uma versão antiga (ou um
|
|
452
|
+
crash dela) pode ter deixado o terminal capturando o clique. */
|
|
453
|
+
this.escreverCru('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?25h\x1b[?1049l');
|
|
446
454
|
}
|
|
447
455
|
|
|
448
456
|
/** Junta as mudanças de um mesmo instante num quadro só. */
|
|
@@ -494,9 +502,21 @@ class Entrada extends EventEmitter {
|
|
|
494
502
|
if (!this.cheia || this.fechado || !this._naTela) return;
|
|
495
503
|
const q = this._quadroCheio();
|
|
496
504
|
this.rolagem = Math.min(this.rolagem, q.rolagemMax);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
505
|
+
/* Só reescreve a linha que MUDOU. Repintar a tela inteira a cada
|
|
506
|
+
quadro (140 ms enquanto trabalha) apagava a seleção do mouse no
|
|
507
|
+
meio do arrasto: o terminal desfaz a seleção quando a linha
|
|
508
|
+
selecionada é reescrita. A conversa parada fica intocada. */
|
|
509
|
+
const antes = this._quadroAnterior;
|
|
510
|
+
const mesmoTamanho = antes && antes.linhas.length === q.linhas.length;
|
|
511
|
+
let corpo = '';
|
|
512
|
+
q.linhas.forEach((l, i) => {
|
|
513
|
+
if (mesmoTamanho && antes.linhas[i] === l) return;
|
|
514
|
+
corpo += `\x1b[${i + 1};1H${l}\x1b[0m\x1b[K`;
|
|
515
|
+
});
|
|
516
|
+
const mesmoCursor = antes && antes.cursor.linha === q.cursor.linha && antes.cursor.coluna === q.cursor.coluna;
|
|
517
|
+
this._quadroAnterior = { linhas: q.linhas, cursor: q.cursor };
|
|
518
|
+
if (!corpo && mesmoCursor) { this.desenhado = { linhas: q.linhas, cursorLinha: q.cursor.linha }; return; }
|
|
519
|
+
const out = (corpo ? '\x1b[?25l' + corpo : '') + `\x1b[${q.cursor.linha + 1};${q.cursor.coluna + 1}H` + (corpo ? '\x1b[?25h' : '');
|
|
500
520
|
this._pintando = true;
|
|
501
521
|
this.escreverCru(out);
|
|
502
522
|
this._pintando = false;
|
|
@@ -692,7 +712,9 @@ class Entrada extends EventEmitter {
|
|
|
692
712
|
}
|
|
693
713
|
|
|
694
714
|
/* A roda do mouse chega como "\x1b[<64;x;yM", e o readline a entrega
|
|
695
|
-
picada, tecla por tecla. Daqui até o M (ou m), tudo é do mouse.
|
|
715
|
+
picada, tecla por tecla. Daqui até o M (ou m), tudo é do mouse.
|
|
716
|
+
Não ligamos mais a captura, mas um terminal que ficou com ela
|
|
717
|
+
ligada (de outro programa) não pode digitar lixo na caixa. */
|
|
696
718
|
if (key && key.code === '[<') { this._mouse = ''; return; }
|
|
697
719
|
if (this._mouse != null) {
|
|
698
720
|
if (/^[mM]$/.test(seq)) {
|
package/lib/estudio/colecao.js
CHANGED
|
@@ -55,6 +55,27 @@ const PT_EN = {
|
|
|
55
55
|
surpreso: 'surprised', cerebro: 'brain', 'cérebro': 'brain', olho: 'eye', mao: 'hand', 'mão': 'hand', joinha: 'thumbs up',
|
|
56
56
|
palmas: 'clapping', aplauso: 'clapping', calendario: 'calendar', 'calendário': 'calendar', saco: 'money bag', banco: 'bank',
|
|
57
57
|
cartao: 'credit card', 'cartão': 'credit card', loja: 'shopping', compras: 'shopping cart', voto: 'ballot box', ferramenta: 'hammer',
|
|
58
|
+
// Comida, objeto e veículo: o que vira personagem ou peça da cena 3D (mundo.js).
|
|
59
|
+
hamburguer: 'hamburger', 'hambúrguer': 'hamburger', batata: 'potato', 'batata frita': 'french fries', sorvete: 'ice cream', bolo: 'birthday cake',
|
|
60
|
+
chocolate: 'chocolate bar', maca: 'red apple', 'maçã': 'red apple', uva: 'grapes', morango: 'strawberry', abacaxi: 'pineapple', melancia: 'watermelon',
|
|
61
|
+
cenoura: 'carrot', ovo: 'egg', queijo: 'cheese wedge', leite: 'glass of milk', cerveja: 'beer mug', vinho: 'wine glass', suco: 'beverage box',
|
|
62
|
+
refrigerante: 'cup with straw', sanduiche: 'sandwich', 'sanduíche': 'sandwich', sushi: 'sushi', 'cachorro-quente': 'hot dog', pipoca: 'popcorn',
|
|
63
|
+
rosquinha: 'doughnut', donut: 'doughnut', biscoito: 'cookie', abacate: 'avocado', coco: 'coconut', taco: 'taco', croissant: 'croissant',
|
|
64
|
+
barraca: 'tent', fogueira: 'fire', palmeira: 'palm tree', violao: 'guitar', 'violão': 'guitar', guitarra: 'guitar', bola: 'soccer ball',
|
|
65
|
+
bicicleta: 'bicycle', moto: 'motorcycle', aviao: 'airplane', 'avião': 'airplane', trem: 'locomotive', onibus: 'bus', 'ônibus': 'bus',
|
|
66
|
+
navio: 'ship', helicoptero: 'helicopter', 'helicóptero': 'helicopter', lapis: 'pencil', 'lápis': 'pencil', caneta: 'pen', mochila: 'backpack',
|
|
67
|
+
chapeu: 'top hat', 'chapéu': 'top hat', oculos: 'glasses', 'óculos': 'glasses', televisao: 'television', 'televisão': 'television', tv: 'television',
|
|
68
|
+
microfone: 'microphone', fone: 'headphone', 'guarda-chuva': 'umbrella', cogumelo: 'mushroom', semaforo: 'vertical traffic light',
|
|
69
|
+
'semáforo': 'vertical traffic light', ampulheta: 'hourglass', lua: 'crescent moon', nuvem: 'cloud', arco_iris: 'rainbow', 'arco-íris': 'rainbow',
|
|
70
|
+
flor: 'blossom', rosa: 'rose', girassol: 'sunflower', 'cacto': 'cactus', vulcao: 'volcano', 'vulcão': 'volcano', castelo: 'castle',
|
|
71
|
+
fantasma: 'ghost', alien: 'alien', 'alienígena': 'alien', et: 'alien', unicornio: 'unicorn', 'unicórnio': 'unicorn', dragao: 'dragon', 'dragão': 'dragon',
|
|
72
|
+
// Bichos que o kit/mundo.jsx não monta de blocos: vêm do ícone 3D (a lula fica de fora: é o presidente).
|
|
73
|
+
polvo: 'octopus', baleia: 'whale', caranguejo: 'crab', borboleta: 'butterfly', abelha: 'honeybee', cobra: 'snake', jacare: 'crocodile',
|
|
74
|
+
'jacaré': 'crocodile', crocodilo: 'crocodile', formiga: 'ant', aranha: 'spider', camelo: 'camel', zebra: 'zebra', hipopotamo: 'hippopotamus',
|
|
75
|
+
'hipopótamo': 'hippopotamus', rinoceronte: 'rhinoceros', canguru: 'kangaroo', coala: 'koala', preguica: 'sloth', 'preguiça': 'sloth',
|
|
76
|
+
joaninha: 'lady beetle', caracol: 'snail', minhoca: 'worm', morcego: 'bat', esquilo: 'chipmunk', rato: 'mouse', hamster: 'hamster',
|
|
77
|
+
lagosta: 'lobster', camarao: 'shrimp', 'camarão': 'shrimp', flamingo: 'flamingo', pavao: 'peacock', 'pavão': 'peacock', cisne: 'swan',
|
|
78
|
+
aguia: 'eagle', 'águia': 'eagle', peru: 'turkey', lhama: 'llama', gorila: 'gorilla', orangotango: 'orangutan', bufalo: 'water buffalo',
|
|
58
79
|
engrenagem: 'gear', ciencia: 'test tube', 'ciência': 'test tube', saude: 'hospital', 'saúde': 'hospital', remedio: 'pill', 'remédio': 'pill',
|
|
59
80
|
};
|
|
60
81
|
|
|
@@ -182,4 +203,16 @@ function cardapio() {
|
|
|
182
203
|
};
|
|
183
204
|
}
|
|
184
205
|
|
|
185
|
-
|
|
206
|
+
/** O ícone 3D que É a coisa (nota de palavra inteira), ou null: "pizza" acha, "joel" não. */
|
|
207
|
+
function melhorIcone3d(termo, piso = 80) {
|
|
208
|
+
const ts = termos(termo);
|
|
209
|
+
if (!ts.length) return null;
|
|
210
|
+
let melhor = null;
|
|
211
|
+
for (const [nome, , kw = ''] of fluent()) {
|
|
212
|
+
const n = Math.max(nota(nome, ts), nota(kw, ts) * 0.8);
|
|
213
|
+
if (n >= piso && (!melhor || n > melhor.n || (n === melhor.n && nome.length < melhor.nome.length))) melhor = { nome, n };
|
|
214
|
+
}
|
|
215
|
+
return melhor ? 'fluent3d:' + melhor.nome : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { melhorIcone3d, procurarIcones3d, procurarAnimacoes, baixar, cardapio, termos, PT_EN };
|