primocode 8.42.2 → 9.1.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 +41 -37
- package/bin/primocode.js +598 -360
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +42 -195
- package/lib/catalogo.js +100 -61
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +576 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +644 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +49 -6
- package/lib/marca.js +4 -1
- package/lib/markdown.js +376 -0
- package/lib/marketplace.js +189 -0
- package/lib/pasta.js +15 -4
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +106 -187
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +199 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +383 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- package/studio/web/view.html +0 -27
package/lib/entrada.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entrada.js — a caixa de digitar do PrimoCode.
|
|
3
|
+
*
|
|
4
|
+
* O que havia antes era o `readline` do Node com um prompt de uma linha só:
|
|
5
|
+
*
|
|
6
|
+
* Auto · ctx 12% · loja ❯ _
|
|
7
|
+
*
|
|
8
|
+
* Funciona, e é o que qualquer programa de terminal faz. O que ele NÃO faz é
|
|
9
|
+
* o que se pediu aqui: uma caixa desenhada, ancorada embaixo, que diz em que
|
|
10
|
+
* pasta o agente está trabalhando e quais teclas existem — sem a pessoa
|
|
11
|
+
* precisar lembrar de nada.
|
|
12
|
+
*
|
|
13
|
+
* ── O QUE O readline NÃO DAVA, E POR ISSO ELE SAIU ───────────────────────
|
|
14
|
+
* Não é gosto. São quatro coisas que ele não faz e que a caixa precisa:
|
|
15
|
+
*
|
|
16
|
+
* 1. desenhar mais de uma linha e continuar sabendo onde o cursor está —
|
|
17
|
+
* o readline pensa em UMA linha de texto, e um pedido de três linhas
|
|
18
|
+
* vira uma linha comprida que rola de lado;
|
|
19
|
+
* 2. manter a caixa ANCORADA embaixo enquanto o agente escreve por cima.
|
|
20
|
+
* Cada `console.log` do agente precisa apagar a caixa, escrever, e
|
|
21
|
+
* redesenhar — senão o texto sai no meio dela;
|
|
22
|
+
* 3. barra de status embaixo do campo, que é onde a pasta e o modo moram;
|
|
23
|
+
* 4. um menu de comandos navegável com as setas.
|
|
24
|
+
*
|
|
25
|
+
* ── A REGRA DA ÂNCORA ────────────────────────────────────────────────────
|
|
26
|
+
* A caixa é sempre a última coisa da tela. Para isso, TODA saída que não é
|
|
27
|
+
* dela passa por `escreverAcima`: apaga a caixa, escreve o texto, redesenha.
|
|
28
|
+
* O `process.stdout.write` é embrulhado enquanto a caixa está no ar, então
|
|
29
|
+
* nenhum `console.log` espalhado pelo programa precisa saber disso.
|
|
30
|
+
*
|
|
31
|
+
* ── O DESENHO É FUNÇÃO PURA ──────────────────────────────────────────────
|
|
32
|
+
* `montar()` recebe um estado e devolve as linhas com a posição do cursor.
|
|
33
|
+
* Ela não toca no terminal, então o teste consegue conferir quadro a quadro
|
|
34
|
+
* (largura estreita, texto que quebra, menu aberto) sem nada na tela. Foi
|
|
35
|
+
* assim que o `/effort` foi feito, pelo mesmo motivo.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
const readline = require('readline');
|
|
41
|
+
const { EventEmitter } = require('events');
|
|
42
|
+
const { c, visibleWidth, truncate } = require('./ui.js');
|
|
43
|
+
|
|
44
|
+
// ── O desenho ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const BORDA = { ne: '╭', no: '╮', se: '╰', so: '╯', h: '─', v: '│' };
|
|
47
|
+
|
|
48
|
+
/** Preenche até `w` colunas VISÍVEIS (códigos de cor não ocupam coluna). */
|
|
49
|
+
function encher(s, w) {
|
|
50
|
+
const falta = w - visibleWidth(s);
|
|
51
|
+
return falta > 0 ? s + ' '.repeat(falta) : s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Quebra o texto em linhas de no máximo `w` colunas, guardando de onde cada
|
|
56
|
+
* uma começa no texto cru.
|
|
57
|
+
*
|
|
58
|
+
* O deslocamento é o que faz o cursor saber em que linha da caixa ele está:
|
|
59
|
+
* sem ele, um pedido de três linhas põe o cursor sempre na primeira.
|
|
60
|
+
*/
|
|
61
|
+
function quebrar(texto, w) {
|
|
62
|
+
const linhas = [];
|
|
63
|
+
let inicio = 0;
|
|
64
|
+
for (const paragrafo of String(texto).split('\n')) {
|
|
65
|
+
let resto = paragrafo;
|
|
66
|
+
let base = inicio;
|
|
67
|
+
do {
|
|
68
|
+
const pedaco = resto.slice(0, w);
|
|
69
|
+
linhas.push({ texto: pedaco, de: base });
|
|
70
|
+
base += pedaco.length;
|
|
71
|
+
resto = resto.slice(w);
|
|
72
|
+
} while (resto.length > 0);
|
|
73
|
+
inicio += paragrafo.length + 1; // +1 é o \n que separa
|
|
74
|
+
}
|
|
75
|
+
return linhas;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* O quadro inteiro: caixa, menu e as duas linhas de status.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} e
|
|
82
|
+
* @param {string} e.texto o que está digitado
|
|
83
|
+
* @param {number} e.cursor posição do cursor dentro de `texto`
|
|
84
|
+
* @param {number} e.colunas largura do terminal
|
|
85
|
+
* @param {string} e.fantasma sugestão esmaecida depois do cursor
|
|
86
|
+
* @param {string} e.vazio o texto de espera, quando não há nada digitado
|
|
87
|
+
* @param {string[]} e.status as duas linhas de baixo, já coloridas
|
|
88
|
+
* @param {object|null} e.menu { itens: [[nome, descricao]], sel }
|
|
89
|
+
* @param {boolean} e.ocupado o agente está trabalhando (a caixa recua)
|
|
90
|
+
* @returns {{linhas: string[], cursorLinha: number, cursorColuna: number}}
|
|
91
|
+
*/
|
|
92
|
+
function montar(e) {
|
|
93
|
+
const colunas = Math.max(24, Math.min(e.colunas || 80, 200));
|
|
94
|
+
const largura = colunas - 1; // a última coluna fica livre: escrever nela
|
|
95
|
+
// faz o terminal quebrar a linha sozinho
|
|
96
|
+
const w = Math.max(8, largura - 3); // espaço + '❯ ' — e mais nada à direita
|
|
97
|
+
|
|
98
|
+
const cursor = Math.max(0, Math.min(e.cursor | 0, String(e.texto).length));
|
|
99
|
+
const linhas = quebrar(e.texto, w);
|
|
100
|
+
|
|
101
|
+
// Em qual linha quebrada o cursor caiu. `<=` no fim de cada linha porque o
|
|
102
|
+
// cursor pode estar logo depois do último caractere dela.
|
|
103
|
+
let cursorLinha = 0;
|
|
104
|
+
let cursorColuna = 0;
|
|
105
|
+
for (let i = 0; i < linhas.length; i++) {
|
|
106
|
+
const l = linhas[i];
|
|
107
|
+
const fim = l.de + l.texto.length;
|
|
108
|
+
if (cursor >= l.de && cursor <= fim) { cursorLinha = i; cursorColuna = cursor - l.de; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* DUAS RÉGUAS, NÃO UMA CAIXA.
|
|
112
|
+
Era uma caixa fechada — cantos e barras laterais. O Claude Code, que é a
|
|
113
|
+
referência pedida, usa só a linha de cima e a de baixo, e a diferença
|
|
114
|
+
não é gosto: a barra lateral direita obriga a reservar duas colunas em
|
|
115
|
+
toda linha e a preencher o resto com espaço, e é ela que faz o texto
|
|
116
|
+
colado parecer desalinhado quando o terminal é estreito. Sem as laterais
|
|
117
|
+
o campo ganha as colunas de volta e a emenda some. */
|
|
118
|
+
const out = [];
|
|
119
|
+
const regua = c.dim(BORDA.h.repeat(largura));
|
|
120
|
+
out.push(regua);
|
|
121
|
+
|
|
122
|
+
const vazio = !String(e.texto).length;
|
|
123
|
+
linhas.forEach((l, i) => {
|
|
124
|
+
const marca = i === 0
|
|
125
|
+
? (e.ocupado ? c.dim('❯ ') : c.brand('❯ '))
|
|
126
|
+
: c.dim('┆ ');
|
|
127
|
+
let conteudo = c.white(l.texto);
|
|
128
|
+
// O fantasma só existe na última linha e só com o cursor no fim dela:
|
|
129
|
+
// desenhado no meio do texto, ele empurraria o que vem depois.
|
|
130
|
+
if (i === linhas.length - 1 && e.fantasma && cursor === String(e.texto).length) {
|
|
131
|
+
conteudo += c.dim(e.fantasma);
|
|
132
|
+
}
|
|
133
|
+
if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
|
|
134
|
+
// Sem `encher`: sem barra à direita não há nada para alinhar, e o
|
|
135
|
+
// espaço sobrando só apagaria o que estivesse embaixo.
|
|
136
|
+
out.push(' ' + marca + conteudo);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
out.push(regua);
|
|
140
|
+
|
|
141
|
+
// ── o menu dos comandos ──────────────────────────────────────────────
|
|
142
|
+
// Fica ABAIXO da caixa, não dentro: dentro, ele empurraria o campo de
|
|
143
|
+
// digitar para cima a cada tecla, e o texto que a pessoa escreve saltaria
|
|
144
|
+
// de lugar enquanto ela escreve.
|
|
145
|
+
if (e.menu && e.menu.itens.length) {
|
|
146
|
+
const cabem = Math.max(1, Math.min(e.menu.itens.length, 8));
|
|
147
|
+
// A janela acompanha a seleção: descer na lista rola, em vez de o
|
|
148
|
+
// item escolhido sumir embaixo.
|
|
149
|
+
let topo = Math.max(0, Math.min(e.menu.sel - Math.floor(cabem / 2),
|
|
150
|
+
e.menu.itens.length - cabem));
|
|
151
|
+
for (let i = topo; i < topo + cabem; i++) {
|
|
152
|
+
const [nome, desc] = e.menu.itens[i];
|
|
153
|
+
const escolhido = i === e.menu.sel;
|
|
154
|
+
const nomeW = 16;
|
|
155
|
+
const corpo = (escolhido ? c.brand('❯ ') : ' ')
|
|
156
|
+
+ (escolhido ? c.bold(c.white(encher(nome, nomeW))) : c.muted(encher(nome, nomeW)))
|
|
157
|
+
+ c.dim(desc || '');
|
|
158
|
+
out.push(' ' + truncate(corpo, largura - 2));
|
|
159
|
+
}
|
|
160
|
+
if (e.menu.itens.length > cabem) {
|
|
161
|
+
out.push(' ' + c.dim(`… e mais ${e.menu.itens.length - cabem}`));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const linha of (e.status || [])) out.push(' ' + linha);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
linhas: out,
|
|
169
|
+
cursorLinha: 1 + cursorLinha, // +1 pela régua de cima
|
|
170
|
+
cursorColuna: 1 + 2 + cursorColuna, // espaço + '❯ '
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── A caixa viva ─────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
class Entrada extends EventEmitter {
|
|
177
|
+
/**
|
|
178
|
+
* @param {object} op
|
|
179
|
+
* @param {()=>string[]} op.status as duas linhas de baixo, sob demanda
|
|
180
|
+
* @param {()=>Array} op.comandos [[nome, descrição]] para o menu e o fantasma
|
|
181
|
+
* @param {string} op.vazio o texto de espera
|
|
182
|
+
*/
|
|
183
|
+
constructor(op = {}) {
|
|
184
|
+
super();
|
|
185
|
+
this.op = op;
|
|
186
|
+
this.line = '';
|
|
187
|
+
this.cursor = 0;
|
|
188
|
+
this.historico = [];
|
|
189
|
+
this.noHistorico = -1;
|
|
190
|
+
this.rascunho = '';
|
|
191
|
+
this.menu = null;
|
|
192
|
+
this.desenhado = null; // {linhas, cursorLinha} do último quadro
|
|
193
|
+
this.ativo = false;
|
|
194
|
+
this.suspenso = false;
|
|
195
|
+
this.ocupado = false;
|
|
196
|
+
this.fechado = false;
|
|
197
|
+
this.colando = false;
|
|
198
|
+
this.tty = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
199
|
+
this.escreverCru = process.stdout.write.bind(process.stdout);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── ciclo de vida ────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
iniciar() {
|
|
205
|
+
if (this.ativo) return this;
|
|
206
|
+
this.ativo = true;
|
|
207
|
+
if (!this.tty) { this._semTerminal(); return this; }
|
|
208
|
+
|
|
209
|
+
readline.emitKeypressEvents(process.stdin);
|
|
210
|
+
try { process.stdin.setRawMode(true); } catch { /* alguns terminais recusam */ }
|
|
211
|
+
process.stdin.resume();
|
|
212
|
+
|
|
213
|
+
/* Colagem entre marcas. Sem isto, colar um texto de trinta linhas faz
|
|
214
|
+
o terminal mandar trinta Enters, e o PrimoCode dispara trinta
|
|
215
|
+
pedidos — um por linha. Com as marcas, o bloco inteiro chega como
|
|
216
|
+
texto e vira UM pedido de várias linhas. */
|
|
217
|
+
this.escreverCru('\x1b[?2004h');
|
|
218
|
+
|
|
219
|
+
this._aoTeclar = (ch, key) => this._tecla(ch, key);
|
|
220
|
+
process.stdin.on('keypress', this._aoTeclar);
|
|
221
|
+
|
|
222
|
+
this._aoRedimensionar = () => { if (this.desenhado) this._redesenhar(); };
|
|
223
|
+
process.stdout.on('resize', this._aoRedimensionar);
|
|
224
|
+
|
|
225
|
+
/* Todo `console.log` do programa passa a apagar a caixa antes de
|
|
226
|
+
escrever, e a redesenhá-la depois. Embrulhar aqui é o que evita ter
|
|
227
|
+
de trocar todas as centenas de `console.log` espalhados pelo
|
|
228
|
+
código — e, mais importante, o que evita que UM esquecido escreva
|
|
229
|
+
por cima da caixa. */
|
|
230
|
+
this._stdoutOriginal = process.stdout.write;
|
|
231
|
+
const eu = this;
|
|
232
|
+
process.stdout.write = function (texto, ...resto) {
|
|
233
|
+
if (eu._pintando) return eu._stdoutOriginal.call(this, texto, ...resto);
|
|
234
|
+
eu._apagar();
|
|
235
|
+
const r = eu._stdoutOriginal.call(this, texto, ...resto);
|
|
236
|
+
/* ── O TEXTO QUE CHEGA EM PEDAÇOS ────────────────────────────
|
|
237
|
+
A resposta do chat sai em fluxo: dezenas de `write` pequenos,
|
|
238
|
+
nenhum deles terminando em quebra de linha. Cada um deixa o
|
|
239
|
+
cursor NO MEIO da linha — e o desenho da caixa começa com
|
|
240
|
+
"\r\x1b[0J", que volta ao início da linha e limpa dali para
|
|
241
|
+
baixo. Ou seja: redesenhar ali apagava o pedaço que acabara de
|
|
242
|
+
ser escrito, e o seguinte apagava o próximo.
|
|
243
|
+
|
|
244
|
+
O efeito para quem usa era o pior possível: no modo chat a
|
|
245
|
+
mensagem ia, o modelo respondia, e a tela não mostrava NADA —
|
|
246
|
+
parecia que o programa tinha ignorado a pessoa.
|
|
247
|
+
|
|
248
|
+
Então a caixa espera a linha fechar. Enquanto o cursor estiver
|
|
249
|
+
no meio de uma, ela não é redesenhada; quando a quebra de linha
|
|
250
|
+
chega (o `fechar()` do fluxo manda uma), ela volta. */
|
|
251
|
+
const t = String(texto == null ? '' : texto);
|
|
252
|
+
eu._meiaLinha = t.length > 0 && !t.endsWith('\n');
|
|
253
|
+
if (!eu._meiaLinha) eu._desenhar();
|
|
254
|
+
return r;
|
|
255
|
+
};
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Sem terminal de verdade (pipe, CI, `primocode "faça x"`) não há caixa
|
|
260
|
+
para desenhar nem tecla para escutar: cada linha que chega é um pedido.
|
|
261
|
+
Desenhar aqui encheria a saída de códigos de cor e de bordas. */
|
|
262
|
+
_semTerminal() {
|
|
263
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
264
|
+
this._rlPipe = rl;
|
|
265
|
+
rl.on('line', (l) => this.emit('line', l));
|
|
266
|
+
rl.on('close', () => { this.fechado = true; this.emit('close'); });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
close() {
|
|
270
|
+
if (this.fechado) return;
|
|
271
|
+
this.fechado = true;
|
|
272
|
+
this.parar();
|
|
273
|
+
this.emit('close');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
parar() {
|
|
277
|
+
if (!this.tty) { if (this._rlPipe) this._rlPipe.close(); return; }
|
|
278
|
+
this._apagar();
|
|
279
|
+
this.escreverCru('\x1b[?2004l');
|
|
280
|
+
if (this._stdoutOriginal) process.stdout.write = this._stdoutOriginal;
|
|
281
|
+
if (this._aoTeclar) process.stdin.removeListener('keypress', this._aoTeclar);
|
|
282
|
+
if (this._aoRedimensionar) process.stdout.removeListener('resize', this._aoRedimensionar);
|
|
283
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
284
|
+
process.stdin.pause();
|
|
285
|
+
this.ativo = false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* Enquanto outro desenho é dono da tela — o painel do /effort, uma tela
|
|
289
|
+
do marketplace — a caixa sai da frente e larga o teclado. Sem isso, os
|
|
290
|
+
dois desenhariam no mesmo lugar e as setas moveriam os dois. */
|
|
291
|
+
suspender() { this.suspenso = true; this._apagar(); }
|
|
292
|
+
retomar() { this.suspenso = false; if (this.ativo && !this.fechado) this._desenhar(); }
|
|
293
|
+
|
|
294
|
+
/** O agente está trabalhando: a caixa esmaece e o Enter não manda nada. */
|
|
295
|
+
trabalhando(sim) {
|
|
296
|
+
if (this.ocupado === !!sim) return;
|
|
297
|
+
this.ocupado = !!sim;
|
|
298
|
+
if (this.desenhado) this._redesenhar();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
prompt() { if (!this.suspenso && this.tty && !this.fechado) this._desenhar(); }
|
|
302
|
+
|
|
303
|
+
/** Escreve texto ACIMA da caixa — é por onde todo o resto do programa fala. */
|
|
304
|
+
escreverAcima(texto) {
|
|
305
|
+
if (!this.desenhado) { this.escreverCru(texto); return; }
|
|
306
|
+
this._apagar();
|
|
307
|
+
this.escreverCru(texto);
|
|
308
|
+
this._desenhar();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Põe texto no campo, como se tivesse sido digitado. */
|
|
312
|
+
write(texto) {
|
|
313
|
+
if (!this.tty) return;
|
|
314
|
+
this.line = this.line.slice(0, this.cursor) + texto + this.line.slice(this.cursor);
|
|
315
|
+
this.cursor += texto.length;
|
|
316
|
+
this._mudou();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this._mudou(); }
|
|
320
|
+
|
|
321
|
+
// ── desenho ──────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
_quadro() {
|
|
324
|
+
return montar({
|
|
325
|
+
texto: this.line,
|
|
326
|
+
cursor: this.cursor,
|
|
327
|
+
colunas: process.stdout.columns || 80,
|
|
328
|
+
fantasma: this._fantasma(),
|
|
329
|
+
vazio: this.op.vazio || '',
|
|
330
|
+
status: this.op.status ? this.op.status() : [],
|
|
331
|
+
menu: this.menu,
|
|
332
|
+
ocupado: this.ocupado,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_desenhar() {
|
|
337
|
+
if (!this.tty || this.suspenso || this.fechado) return;
|
|
338
|
+
const q = this._quadro();
|
|
339
|
+
this._pintando = true;
|
|
340
|
+
/* Cursor no meio de uma linha (fluxo que ainda não fechou): desce uma
|
|
341
|
+
antes de desenhar. Vale para quem chama `_desenhar` por fora do
|
|
342
|
+
embrulho do stdout — `prompt()`, o fim de uma tela suspensa — e é o
|
|
343
|
+
que garante que a caixa nunca começa em cima de texto pela metade. */
|
|
344
|
+
if (this._meiaLinha) { this.escreverCru('\n'); this._meiaLinha = false; }
|
|
345
|
+
// `\x1b[0J` limpa daqui até o fim da tela: sem isso, um quadro mais
|
|
346
|
+
// curto que o anterior (menu que fechou) deixa lixo embaixo.
|
|
347
|
+
this.escreverCru('\r\x1b[0J' + q.linhas.join('\n'));
|
|
348
|
+
const paraCima = q.linhas.length - 1 - q.cursorLinha;
|
|
349
|
+
let volta = '\r';
|
|
350
|
+
if (paraCima > 0) volta += `\x1b[${paraCima}A`;
|
|
351
|
+
if (q.cursorColuna > 0) volta += `\x1b[${q.cursorColuna}C`;
|
|
352
|
+
this.escreverCru(volta);
|
|
353
|
+
this._pintando = false;
|
|
354
|
+
this.desenhado = q;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_apagar() {
|
|
358
|
+
if (!this.desenhado || !this.tty) return;
|
|
359
|
+
this._pintando = true;
|
|
360
|
+
// Sobe até a primeira linha do quadro e limpa daí para baixo.
|
|
361
|
+
const acima = this.desenhado.cursorLinha;
|
|
362
|
+
this.escreverCru((acima > 0 ? `\x1b[${acima}A` : '') + '\r\x1b[0J');
|
|
363
|
+
this._pintando = false;
|
|
364
|
+
this.desenhado = null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_redesenhar() { this._apagar(); this._desenhar(); }
|
|
368
|
+
|
|
369
|
+
_mudou() {
|
|
370
|
+
if (this.menu) this._filtrarMenu();
|
|
371
|
+
this._redesenhar();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── o fantasma e o menu ──────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
_fantasma() {
|
|
377
|
+
if (this.ocupado) return '';
|
|
378
|
+
const linha = this.line;
|
|
379
|
+
if (!linha.startsWith('/') || /\s/.test(linha) || linha.length < 2) return '';
|
|
380
|
+
if (this.cursor !== linha.length) return '';
|
|
381
|
+
const lista = this.op.comandos ? this.op.comandos() : [];
|
|
382
|
+
const baixa = linha.toLowerCase();
|
|
383
|
+
const hit = lista.map(([n]) => n).find((n) => n.startsWith(baixa) && n.length > linha.length);
|
|
384
|
+
return hit ? hit.slice(linha.length) : '';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
_filtrarMenu() {
|
|
388
|
+
const lista = this.op.comandos ? this.op.comandos() : [];
|
|
389
|
+
const baixa = this.line.toLowerCase();
|
|
390
|
+
const itens = this.line.startsWith('/') && !/\s/.test(this.line)
|
|
391
|
+
? lista.filter(([n]) => n.startsWith(baixa))
|
|
392
|
+
: [];
|
|
393
|
+
if (!itens.length) { this.menu = null; return; }
|
|
394
|
+
const sel = this.menu ? Math.min(this.menu.sel, itens.length - 1) : 0;
|
|
395
|
+
this.menu = { itens, sel };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ── teclado ──────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
_tecla(ch, key) {
|
|
401
|
+
if (this.suspenso || this.fechado) return;
|
|
402
|
+
const seq = (key && key.sequence) || ch || '';
|
|
403
|
+
|
|
404
|
+
// Colagem entre marcas: tudo que vier até a marca de fim é texto.
|
|
405
|
+
if (seq.includes('\x1b[200~')) { this.colando = true; }
|
|
406
|
+
if (this.colando) {
|
|
407
|
+
const limpo = seq.replace('\x1b[200~', '').replace('\x1b[201~', '');
|
|
408
|
+
if (limpo) this.write(limpo.replace(/\r/g, '\n'));
|
|
409
|
+
if (seq.includes('\x1b[201~')) this.colando = false;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!key) { if (ch) this.write(ch); return; }
|
|
414
|
+
|
|
415
|
+
// Esc Esc e o resto das teclas de fora: quem escuta decide. A caixa
|
|
416
|
+
// não sabe o que é "parar o agente" — isso é assunto do programa.
|
|
417
|
+
if (key.name === 'escape') {
|
|
418
|
+
if (this.menu) { this.menu = null; this._redesenhar(); return; }
|
|
419
|
+
this.emit('escape');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (key.ctrl) return this._ctrl(key);
|
|
424
|
+
|
|
425
|
+
switch (key.name) {
|
|
426
|
+
case 'return':
|
|
427
|
+
case 'enter':
|
|
428
|
+
/* Alt+Enter (e a barra invertida no fim) quebram linha em vez
|
|
429
|
+
de mandar. É o que deixa escrever um pedido de parágrafo
|
|
430
|
+
sem ele sair pela metade no primeiro Enter. */
|
|
431
|
+
if (key.meta) return this._novaLinha();
|
|
432
|
+
if (this.line.endsWith('\\')) {
|
|
433
|
+
this.line = this.line.slice(0, -1);
|
|
434
|
+
this.cursor = Math.max(0, this.cursor - 1);
|
|
435
|
+
return this._novaLinha();
|
|
436
|
+
}
|
|
437
|
+
return this._enviar();
|
|
438
|
+
|
|
439
|
+
case 'tab': {
|
|
440
|
+
if (this.menu) return this._escolherDoMenu();
|
|
441
|
+
const f = this._fantasma();
|
|
442
|
+
if (f) return this.write(f);
|
|
443
|
+
if (this.line.startsWith('/')) { this._filtrarMenu(); this._redesenhar(); }
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
case 'backspace':
|
|
448
|
+
if (this.cursor > 0) {
|
|
449
|
+
this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
|
|
450
|
+
this.cursor--;
|
|
451
|
+
this._mudou();
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
|
|
455
|
+
case 'delete':
|
|
456
|
+
if (this.cursor < this.line.length) {
|
|
457
|
+
this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor + 1);
|
|
458
|
+
this._mudou();
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
|
|
462
|
+
case 'left': if (this.cursor > 0) { this.cursor--; this._redesenhar(); } return;
|
|
463
|
+
case 'right': {
|
|
464
|
+
// No fim da linha, a seta direita aceita a sugestão inteira —
|
|
465
|
+
// era assim antes da caixa, e o dedo de quem usa já sabe.
|
|
466
|
+
const f = this._fantasma();
|
|
467
|
+
if (f) return this.write(f);
|
|
468
|
+
if (this.cursor < this.line.length) { this.cursor++; this._redesenhar(); }
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
case 'home': this.cursor = 0; this._redesenhar(); return;
|
|
472
|
+
case 'end': this.cursor = this.line.length; this._redesenhar(); return;
|
|
473
|
+
|
|
474
|
+
case 'up':
|
|
475
|
+
if (this.menu) { this.menu.sel = Math.max(0, this.menu.sel - 1); return this._redesenhar(); }
|
|
476
|
+
return this._historico(-1);
|
|
477
|
+
case 'down':
|
|
478
|
+
if (this.menu) {
|
|
479
|
+
this.menu.sel = Math.min(this.menu.itens.length - 1, this.menu.sel + 1);
|
|
480
|
+
return this._redesenhar();
|
|
481
|
+
}
|
|
482
|
+
return this._historico(1);
|
|
483
|
+
|
|
484
|
+
default:
|
|
485
|
+
if (seq && !key.meta && seq >= ' ' && seq !== '\x7f') this.write(seq);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_ctrl(key) {
|
|
490
|
+
switch (key.name) {
|
|
491
|
+
case 'c':
|
|
492
|
+
// Com texto no campo, Ctrl+C limpa o que foi digitado — sair
|
|
493
|
+
// por acidente no meio de um pedido longo é perder o pedido.
|
|
494
|
+
if (this.line) return this.limpar();
|
|
495
|
+
this.emit('sigint');
|
|
496
|
+
return;
|
|
497
|
+
case 'd':
|
|
498
|
+
if (!this.line) return this.close();
|
|
499
|
+
return;
|
|
500
|
+
case 'u': this.line = this.line.slice(this.cursor); this.cursor = 0; return this._mudou();
|
|
501
|
+
case 'k': this.line = this.line.slice(0, this.cursor); return this._mudou();
|
|
502
|
+
case 'a': this.cursor = 0; return this._redesenhar();
|
|
503
|
+
case 'e': this.cursor = this.line.length; return this._redesenhar();
|
|
504
|
+
case 'w': {
|
|
505
|
+
const antes = this.line.slice(0, this.cursor).replace(/\s*\S+$/, '');
|
|
506
|
+
const depois = this.line.slice(this.cursor);
|
|
507
|
+
this.line = antes + depois;
|
|
508
|
+
this.cursor = antes.length;
|
|
509
|
+
return this._mudou();
|
|
510
|
+
}
|
|
511
|
+
case 'l': this._apagar(); this.escreverCru('\x1b[2J\x1b[H'); return this._desenhar();
|
|
512
|
+
case 'j': return this._novaLinha();
|
|
513
|
+
default: return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
_novaLinha() {
|
|
518
|
+
this.line = this.line.slice(0, this.cursor) + '\n' + this.line.slice(this.cursor);
|
|
519
|
+
this.cursor++;
|
|
520
|
+
this._mudou();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
_escolherDoMenu() {
|
|
524
|
+
const [nome] = this.menu.itens[this.menu.sel];
|
|
525
|
+
this.line = nome;
|
|
526
|
+
this.cursor = nome.length;
|
|
527
|
+
this.menu = null;
|
|
528
|
+
this._redesenhar();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
_historico(passo) {
|
|
532
|
+
if (!this.historico.length) return;
|
|
533
|
+
if (this.noHistorico === -1) {
|
|
534
|
+
if (passo > 0) return;
|
|
535
|
+
this.rascunho = this.line;
|
|
536
|
+
this.noHistorico = this.historico.length;
|
|
537
|
+
}
|
|
538
|
+
const i = this.noHistorico + passo;
|
|
539
|
+
if (i >= this.historico.length) {
|
|
540
|
+
this.noHistorico = -1;
|
|
541
|
+
this.line = this.rascunho;
|
|
542
|
+
} else if (i < 0) {
|
|
543
|
+
return;
|
|
544
|
+
} else {
|
|
545
|
+
this.noHistorico = i;
|
|
546
|
+
this.line = this.historico[i];
|
|
547
|
+
}
|
|
548
|
+
this.cursor = this.line.length;
|
|
549
|
+
this._redesenhar();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
_enviar() {
|
|
553
|
+
if (this.menu) return this._escolherDoMenu();
|
|
554
|
+
const texto = this.line;
|
|
555
|
+
this.line = '';
|
|
556
|
+
this.cursor = 0;
|
|
557
|
+
this.menu = null;
|
|
558
|
+
this.noHistorico = -1;
|
|
559
|
+
|
|
560
|
+
/* O que foi mandado sai da caixa e vira linha do histórico da tela —
|
|
561
|
+
senão o pedido some no instante em que a resposta começa, e quem
|
|
562
|
+
rola para cima não acha o que perguntou. */
|
|
563
|
+
this._apagar();
|
|
564
|
+
if (texto.trim()) {
|
|
565
|
+
const marca = c.brand('❯ ');
|
|
566
|
+
this.escreverCru(texto.split('\n').map((l, i) => (i ? ' ' : marca) + c.white(l)).join('\n') + '\n');
|
|
567
|
+
if (this.historico[this.historico.length - 1] !== texto) this.historico.push(texto);
|
|
568
|
+
}
|
|
569
|
+
this._desenhar();
|
|
570
|
+
this.emit('line', texto);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function criar(op) { return new Entrada(op).iniciar(); }
|
|
575
|
+
|
|
576
|
+
module.exports = { criar, Entrada, montar, quebrar, encher };
|
package/lib/especialistas.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* generalista escreve o CSS que qualquer um escreveria. O briefing é o que um
|
|
12
12
|
* sênior daquela área diria a um colega antes de ele começar — o que olhar
|
|
13
13
|
* primeiro, o que nunca fazer, como PROVAR que acabou. Ele vai na frente da
|
|
14
|
-
* tarefa, na MESMA mensagem
|
|
15
|
-
*
|
|
14
|
+
* tarefa, na MESMA mensagem — assim o servidor não precisa conhecer os
|
|
15
|
+
* especialistas.
|
|
16
16
|
*
|
|
17
17
|
* ── CURTO DE PROPÓSITO ──────────────────────────────────────────────────
|
|
18
18
|
* Cada briefing cabe em ~800 caracteres. O subagente vive no mesmo teto de
|