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.
Files changed (104) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +42 -195
  7. package/lib/catalogo.js +100 -61
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +576 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +644 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +49 -6
  23. package/lib/marca.js +4 -1
  24. package/lib/markdown.js +376 -0
  25. package/lib/marketplace.js +189 -0
  26. package/lib/pasta.js +15 -4
  27. package/lib/permissao.js +137 -0
  28. package/lib/registro.js +229 -0
  29. package/lib/skills.js +329 -0
  30. package/lib/tools.js +106 -187
  31. package/lib/ui.js +7 -3
  32. package/lib/voz.js +239 -0
  33. package/package.json +12 -3
  34. package/skills/acessibilidade/SKILL.md +28 -0
  35. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  36. package/skills/depurar-de-verdade/SKILL.md +29 -0
  37. package/skills/editar-meu-video/SKILL.md +59 -0
  38. package/skills/escrever-em-portugues/SKILL.md +28 -0
  39. package/skills/git-limpo/SKILL.md +28 -0
  40. package/skills/motion-de-primeira/SKILL.md +62 -0
  41. package/skills/pagina-que-converte/SKILL.md +28 -0
  42. package/skills/performance-web/SKILL.md +28 -0
  43. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  44. package/skills/slides-que-prendem/SKILL.md +40 -0
  45. package/studio/edge.py +16 -66
  46. package/studio/narrar.py +274 -0
  47. package/studio/pessoa.py +199 -0
  48. package/voz/primo_voz/__init__.py +0 -0
  49. package/voz/primo_voz/__main__.py +383 -0
  50. package/voz/primo_voz/escuta.py +235 -0
  51. package/voz/primo_voz/orbe.py +216 -0
  52. package/lib/audio-livre.js +0 -177
  53. package/lib/chaves.js +0 -149
  54. package/lib/claude-engine.js +0 -631
  55. package/lib/codex-engine.js +0 -208
  56. package/lib/remotion.js +0 -663
  57. package/lib/studio.js +0 -960
  58. package/studio/PRIMOCODE.md +0 -58
  59. package/studio/README.md +0 -154
  60. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  61. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  62. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  63. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  64. package/studio/ferramentas/00-protocolo.md +0 -125
  65. package/studio/ferramentas/01-modelos.md +0 -270
  66. package/studio/ferramentas/README.md +0 -32
  67. package/studio/ferramentas/corte.md +0 -919
  68. package/studio/ferramentas/grade.md +0 -92
  69. package/studio/ferramentas/prisma.md +0 -107
  70. package/studio/ferramentas/prosa.md +0 -86
  71. package/studio/ferramentas/tela.md +0 -93
  72. package/studio/ferramentas/traco.md +0 -79
  73. package/studio/server.py +0 -2254
  74. package/studio/studio.log +0 -57
  75. package/studio/voz.py +0 -388
  76. package/studio/web/analise.html +0 -52
  77. package/studio/web/core.css +0 -1167
  78. package/studio/web/fontes/bebas.woff2 +0 -0
  79. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  80. package/studio/web/fontes/inter-400.woff2 +0 -0
  81. package/studio/web/fontes/inter-700.woff2 +0 -0
  82. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  83. package/studio/web/fontes/playfair-700.woff2 +0 -0
  84. package/studio/web/fontes.css +0 -23
  85. package/studio/web/home.html +0 -217
  86. package/studio/web/js/analise.js +0 -510
  87. package/studio/web/js/audio.js +0 -607
  88. package/studio/web/js/editor.js +0 -1802
  89. package/studio/web/js/exportar.js +0 -222
  90. package/studio/web/js/ferramentas.js +0 -108
  91. package/studio/web/js/icones.js +0 -120
  92. package/studio/web/js/midia.js +0 -157
  93. package/studio/web/js/narracao.js +0 -325
  94. package/studio/web/js/player.js +0 -903
  95. package/studio/web/js/render-grade.js +0 -243
  96. package/studio/web/js/render-prosa.js +0 -104
  97. package/studio/web/js/render.js +0 -1852
  98. package/studio/web/js/sfx.js +0 -233
  99. package/studio/web/js/video.js +0 -464
  100. package/studio/web/js/voz-neural.js +0 -152
  101. package/studio/web/render.css +0 -1352
  102. package/studio/web/tf.html +0 -15
  103. package/studio/web/tool.html +0 -78
  104. 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 };
@@ -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: o servidor não precisa conhecer os especialistas,
15
- * e o /claude e o /codex os recebem igual.
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