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/bin/primocode.js CHANGED
@@ -14,6 +14,7 @@ const os = require('os');
14
14
 
15
15
  const pkg = require('../package.json');
16
16
  const ui = require('../lib/ui');
17
+ const entrada = require('../lib/entrada');
17
18
  const { c, welcomeBox, Spinner } = ui;
18
19
  const { escolherModo } = require('../lib/modo.js');
19
20
  const api = require('../lib/api');
@@ -21,14 +22,16 @@ const tools = require('../lib/tools');
21
22
  const cfg = require('../lib/config');
22
23
  const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
23
24
  const fala = require('../lib/fala');
24
- const claudeEngine = require('../lib/claude-engine');
25
- const codexEngine = require('../lib/codex-engine');
26
- const chaves = require('../lib/chaves');
27
25
  const boasVindas = require('../lib/boas-vindas');
28
26
  const pasta = require('../lib/pasta');
29
27
  const catalogo = require('../lib/catalogo');
30
28
  const effort = require('../lib/effort');
31
29
  const memoria = require('../lib/memoria-conversa');
30
+ const registro = require('../lib/registro');
31
+ const permissao = require('../lib/permissao');
32
+ const skills = require('../lib/skills');
33
+ const marketplace = require('../lib/marketplace');
34
+ const vozModo = require('../lib/voz');
32
35
  const appLib = require('../lib/app');
33
36
  const conta = require('../lib/conta');
34
37
  const nuvem = require('../lib/nuvem');
@@ -71,10 +74,14 @@ const state = {
71
74
  liberado: true,
72
75
  motivoBloqueio: null,
73
76
  lastResponse: '',
77
+ trabalhando: false, // o agente está no meio de um pedido (a barra muda)
78
+ /* A autorização para mexer no computador. Vale pela SESSÃO e morre com
79
+ ela: não há arquivo de "sempre permitir" de propósito — autorização
80
+ permanente é exatamente a que ninguém lembra de ter dado. */
81
+ controle: permissao.novo(),
74
82
  currentProject: null,
75
83
  workDir: null, // pasta onde o agente trabalha nesta sessão
76
84
  workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
77
- engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude) | 'codex' (/codex)
78
85
  };
79
86
 
80
87
  // A janela do /rc, quando aberta. Uma por sessão: o segundo /rc traz a que
@@ -96,9 +103,6 @@ ${c.bold('Comandos')}
96
103
  ${c.brand('/run')} <comando> executa no shell do projeto
97
104
  ${c.brand('/open')} <arq|url> abre no navegador
98
105
  ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
99
- ${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
100
- ${c.brand('/gpt')} usa o GPT (Codex) da SUA conta como motor
101
- ${c.brand('/key')} <provedor> sua própria chave de API (Claude, GPT)
102
106
  ${c.brand('/connect')} entra na sua conta · ${c.brand('/account')} mostra o plano
103
107
  ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
104
108
  ${c.brand('/logout')} desconecta este computador da conta
@@ -143,14 +147,16 @@ const SLASH_COMANDOS = [
143
147
  ['/run', 'roda um comando'],
144
148
  ['/open', 'abre no navegador'],
145
149
  ['/rc', 'abre a janela do PrimoCode'],
146
- ['/claude', 'usa o Claude Code da sua conta como motor'],
147
- ['/gpt', 'usa o GPT (Codex) da sua conta como motor'],
148
- ['/key', 'sua própria chave de API (Claude, Codex)'],
149
150
  ['/connect', 'entra na sua conta Conecta Primo'],
150
151
  ['/account', 'sua conta, plano e computadores conectados'],
151
152
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
152
153
  ['/logout', 'desconecta este computador da conta'],
154
+ ['/voice', 'o Primo Code em modo voz: você fala, ele faz'],
155
+ ['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
156
+ ['/marketplace', 'a tela das skills: navegue, ligue, crie'],
157
+ ['/install-skill', 'instala uma skill de um arquivo'],
153
158
  ['/status', 'checa o servidor'],
159
+ ['/connectors', 'quem está atendendo os seus pedidos'],
154
160
  ['/name', 'como ele te chama'],
155
161
  ['/mascot', 'primo | fox | cat'],
156
162
  ['/config', 'suas preferências'],
@@ -161,15 +167,6 @@ const SLASH_COMANDOS = [
161
167
  ['/exit', 'sair'],
162
168
  ];
163
169
 
164
- // O completer do readline: Tab completa o que der. Só age na palavra do
165
- // comando — depois do primeiro espaço o argumento é seu, não dele.
166
- function completarSlash(line) {
167
- if (!line.startsWith('/') || /\s/.test(line)) return [[], line];
168
- const baixa = line.toLowerCase();
169
- const acertos = SLASH_COMANDOS.map(([nome]) => nome).filter((nome) => nome.startsWith(baixa));
170
- return [acertos, line];
171
- }
172
-
173
170
  // ── /effort: o slider de esforço (estilo Claude Code) ──────────────────
174
171
  // Régua com os níveis, o marcador ▲ embaixo do escolhido, "Faster" à
175
172
  // esquerda e "Smarter" à direita. ←/→ move, Enter confirma, Esc cancela.
@@ -221,72 +218,13 @@ function sliderEffort(atual) {
221
218
  if (anima) { clearInterval(anima); anima = null; }
222
219
  process.stdin.removeListener('keypress', aoTeclar);
223
220
  sliderAtivo = false;
221
+ if (rl && rl.retomar) rl.retomar();
224
222
  resolve(valor);
225
223
  };
226
224
  sliderAtivo = true;
227
- process.stdin.on('keypress', aoTeclar);
228
- });
229
- }
230
-
231
- /* ── A LISTA COM AS SETINHAS ─────────────────────────────────────────────
232
- *
233
- * "O usuário seleciona usando as setinhas, igual você."
234
- *
235
- * ↑↓ anda, Enter confirma, Esc fica com o primeiro item. O número também
236
- * funciona — quem já sabe qual quer não precisa navegar até lá.
237
- *
238
- * Devolve `undefined` quando não há terminal de verdade: aí quem chamou cai
239
- * na lista numerada, que funciona em pipe e em CI. `null` é outra coisa —
240
- * é Esc, que É uma resposta ("tanto faz, a primeira").
241
- */
242
- function menuSetas({ titulo, ajuda, lista }) {
243
- return new Promise((resolve) => {
244
- if (!process.stdin.isTTY || !process.stdout.isTTY) return resolve(undefined);
245
- let idx = 0;
246
- const linhas = () => {
247
- const fora = [''];
248
- fora.push(c.bold(c.brand(' ' + titulo)));
249
- if (ajuda) fora.push(c.muted(' ' + ajuda));
250
- fora.push('');
251
- lista.forEach((e, i) => {
252
- const aqui = i === idx;
253
- fora.push((aqui ? c.brand(' ❯ ') : ' ') + (aqui ? c.bold(c.white(e.titulo)) : c.muted(e.titulo)));
254
- fora.push(' ' + c.dim(e.detalhe || ''));
255
- });
256
- fora.push('');
257
- return fora;
258
- };
259
- const ALTURA = linhas().length;
260
-
261
- const pintar = (primeira) => {
262
- if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
263
- process.stdout.write(linhas().map((l) => '\x1b[2K' + l).join('\n') + '\n');
264
- };
265
-
266
- const encerrar = (valor) => {
267
- process.stdin.removeListener('keypress', aoTeclar);
268
- sliderAtivo = false;
269
- try { process.stdout.write('\x1b[?25h'); } catch {}
270
- resolve(valor);
271
- };
272
- const aoTeclar = (ch, key) => {
273
- if (!key) return;
274
- if (key.name === 'down' || key.name === 'j' || (key.name === 'tab' && !key.shift)) {
275
- idx = (idx + 1) % lista.length; return pintar(false);
276
- }
277
- if (key.name === 'up' || key.name === 'k' || (key.name === 'tab' && key.shift)) {
278
- idx = (idx - 1 + lista.length) % lista.length; return pintar(false);
279
- }
280
- if (/^[1-9]$/.test(ch || '') && Number(ch) <= lista.length) {
281
- idx = Number(ch) - 1; return pintar(false);
282
- }
283
- if (key.name === 'return' || key.name === 'enter') return encerrar(lista[idx].id);
284
- if (key.name === 'escape' || (key.ctrl && key.name === 'c')) return encerrar(null);
285
- };
286
-
287
- sliderAtivo = true; // enquanto está no ar, o resto do teclado se cala
288
- try { process.stdout.write('\x1b[?25l'); } catch {}
289
- pintar(true);
225
+ // A caixa sai da frente e larga o teclado: dois desenhos no mesmo
226
+ // lugar, e as setas moveriam os dois ao mesmo tempo.
227
+ if (rl && rl.suspender) rl.suspender();
290
228
  process.stdin.on('keypress', aoTeclar);
291
229
  });
292
230
  }
@@ -314,56 +252,99 @@ function mostrarMenuSlash() {
314
252
 
315
253
  let rl = null;
316
254
 
317
- function makePrompt() {
255
+ /* ── A BARRA DE STATUS ───────────────────────────────────────────────────
256
+ * As duas linhas embaixo da caixa de digitar. Elas existem para responder,
257
+ * sem a pessoa perguntar, as coisas que ela erra quando não sabe:
258
+ *
259
+ * · EM QUE PASTA ele está mexendo — de longe o que mais confunde, porque
260
+ * o PrimoCode NÃO trabalha na pasta de onde você o abriu;
261
+ * · em que ramo do git, quando a pasta é um repositório: um agente que
262
+ * escreve arquivo no `main` sem ninguém perceber é o pior tipo de
263
+ * surpresa;
264
+ * · quanto da conversa já foi gasto, para o /compact chegar antes do teto;
265
+ * · qual o modo e qual o esforço, que mudam o que ele pode fazer.
266
+ *
267
+ * A segunda linha é a dos atalhos. Ela repete o que existe porque atalho que
268
+ * a pessoa não vê é atalho que não existe.
269
+ */
270
+
271
+ // `git rev-parse` a cada quadro seria um processo por tecla digitada. O ramo
272
+ // muda devagar; cinco segundos de atraso não confundem ninguém.
273
+ const ramoCache = new Map();
274
+ function ramoDoGit(dir) {
275
+ if (!dir) return '';
276
+ const guardado = ramoCache.get(dir);
277
+ if (guardado && Date.now() - guardado.quando < 5000) return guardado.ramo;
278
+ let ramo = '';
279
+ try {
280
+ ramo = require('child_process').execSync('git rev-parse --abbrev-ref HEAD', {
281
+ cwd: dir, stdio: ['ignore', 'pipe', 'ignore'], timeout: 1500,
282
+ }).toString().trim();
283
+ } catch { ramo = ''; }
284
+ ramoCache.set(dir, { quando: Date.now(), ramo });
285
+ return ramo;
286
+ }
287
+
288
+ function barraDeStatus() {
289
+ const partes = [];
290
+
291
+ /* O CONTEXTO. Só aparece depois que há conversa: um "ctx 0%" na abertura
292
+ é ruído, e ruído que aparece sempre é o que a pessoa aprende a não
293
+ enxergar — inclusive quando marcar 90%. Âmbar a partir de 70% e
294
+ vermelho a partir de 100%, que é quando `/compact` deixa de ser
295
+ conselho e vira o que fazer. */
296
+ const uso = memoria.usoDoContexto(state.memoria, state.history);
297
+ if (uso.pct >= 5) {
298
+ partes.push(uso.pct >= 100 ? c.err(`${uso.pct}% ctx`)
299
+ : uso.pct >= 70 ? c.warn(`${uso.pct}% ctx`)
300
+ : c.dim(`${uso.pct}% ctx`));
301
+ }
302
+
303
+ // O esforço, que é o que escolhe o modelo. Não há provedor para mostrar:
304
+ // a API vem com o produto.
305
+ partes.push(c.muted(effort.normalizar(state.config.effort)));
306
+
307
+ // A PASTA. O nome dela, e o caminho curto quando não é um projeto — quem
308
+ // apontou com /dir para fora do ~/primocode precisa ver onde está.
309
+ const dir = state.workDir || process.cwd();
310
+ partes.push(c.brandDim(shortPath(dir)));
311
+
312
+ const ramo = ramoDoGit(dir);
313
+ if (ramo) partes.push(c.dim('⎇ ' + ramo));
314
+
315
+ if (state.attachedFiles.length) {
316
+ partes.push(c.muted(`+${state.attachedFiles.length} anexo${state.attachedFiles.length > 1 ? 's' : ''}`));
317
+ }
318
+
319
+ /* Enquanto o controle do computador está liberado, isso fica escrito na
320
+ barra. Uma autorização que some da vista é uma autorização que a pessoa
321
+ esquece que deu — e ela vale até o PrimoCode fechar. */
322
+ if (state.controle && state.controle.liberado) partes.push(c.warn('controle do pc'));
323
+
318
324
  // No auto o rótulo mostra "Auto" e, depois do primeiro pedido, qual modo
319
325
  // ele acabou usando — senão você nunca sabe quem respondeu.
320
- let modeTag;
326
+ let modo;
321
327
  if (state.mode === 'auto') {
322
- const ultimo = state.modoEfetivo === 'primocode' ? c.warn('primo') : state.modoEfetivo === 'chat' ? c.brandDim('chat') : '';
323
- modeTag = c.brand('Auto') + (ultimo ? c.dim('·') + ultimo : '');
328
+ const ultimo = state.modoEfetivo === 'primocode' ? c.warn('primo')
329
+ : state.modoEfetivo === 'chat' ? c.brandDim('chat') : '';
330
+ modo = c.brand('auto') + (ultimo ? c.dim('·') + ultimo : '');
324
331
  } else {
325
- modeTag = state.mode === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
332
+ modo = state.mode === 'primocode' ? c.warn('primo code') : c.brandDim('chat');
326
333
  }
327
- const proj = state.currentProject ? c.dim(` ${state.currentProject}`) : '';
334
+ partes.push(modo);
328
335
 
329
- /* O CONTEXTO na barra, como o pedido pede. Só aparece depois que há
330
- conversa: um "ctx 0%" na abertura é ruído, e ruído que aparece sempre é
331
- o que a pessoa aprende a não enxergar — inclusive quando marcar 90%.
332
- Fica âmbar a partir de 70% e vermelho a partir de 100%, que é quando
333
- `/compact` deixa de ser conselho e vira o que fazer. */
334
- const uso = memoria.usoDoContexto(state.memoria, state.history);
335
- const ctx = uso.pct < 5 ? '' : c.dim(' · ')
336
- + (uso.pct >= 100 ? c.err(`ctx ${uso.pct}%`)
337
- : uso.pct >= 70 ? c.warn(`ctx ${uso.pct}%`)
338
- : c.dim(`ctx ${uso.pct}%`));
339
- // O modelo ativo: qual motor está pensando. No padrão é o do servidor.
340
- const motor = state.engine === 'claude-code' ? c.dim(' · ') + c.dim('claude')
341
- : state.engine === 'codex' ? c.dim(' · ') + c.dim('gpt') : '';
342
- const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
343
- return `\n${c.muted(modeTag)}${motor}${ctx}${proj}${att} ${c.brand('❯')} `;
344
- }
345
- function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
336
+ const primeira = partes.join(c.dim(' | '));
346
337
 
347
- /**
348
- * O portão do plano (`state.liberado`) SÓ era decidido uma vez, na abertura.
349
- * Quem conectava DEPOIS — `/connect` manual, `/connect trocar`, `/logout` —
350
- * mudava a conta de verdade e o portão continuava com a resposta antiga.
351
- *
352
- * "conecta mas depois de uns 10 segundos desconecta": era exatamente isto.
353
- * A pessoa via "Conectado. Plano premium" — real, acabou de vir do
354
- * servidor — e no pedido seguinte caía em "O PrimoCode pede o plano
355
- * Premium ou Super", porque `state.liberado` tinha ficado `false` de uma
356
- * tentativa anterior (sem plano, ou login que não deu) e nada o atualizava.
357
- *
358
- * Toda vez que a conexão muda, chame isto com o que se sabe AGORA — o
359
- * `{ pro }` que já veio de `conta.entrar()` ou de `conta.atual()`, sem ir à
360
- * rede de novo: essas chamadas já confirmaram o servidor há segundos.
361
- */
362
- function sincronizarLiberacao({ pro, motivo } = {}) {
363
- state.liberado = !!pro;
364
- state.motivoBloqueio = pro ? null : (motivo || 'sem-plano');
338
+ const atalhos = state.trabalhando
339
+ ? c.warn('▸▸ trabalhando') + c.dim(' (Esc Esc interrompe)')
340
+ : c.dim('/ comandos · ↑ histórico · \\ ou alt+enter quebra linha · Esc Esc interrompe');
341
+
342
+ return [primeira, atalhos];
365
343
  }
366
344
 
345
+ // A barra se redesenha sozinha a cada quadro; isto só pede um quadro novo.
346
+ function refreshPrompt() { if (rl) rl.prompt(); }
347
+
367
348
  // Enquanto uma pergunta está no ar, a próxima linha digitada é a RESPOSTA dela
368
349
  // — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
369
350
  let pendingAsk = null;
@@ -545,6 +526,195 @@ function prepararPasta(userText) {
545
526
  return plan;
546
527
  }
547
528
 
529
+ /* ── O PEDIDO PARA MEXER NO COMPUTADOR ───────────────────────────────────
530
+ * Três respostas: agora, a sessão inteira, não.
531
+ *
532
+ * A do meio existe porque uma tarefa de desktop são dez, vinte ações. Pedir a
533
+ * cada clique transforma a permissão num botão que se aperta sem ler — o
534
+ * mesmo que não pedir, com uma etapa a mais. Uma vez, com o que ele vai fazer
535
+ * escrito, é a pergunta que ainda é lida.
536
+ *
537
+ * Enter sozinho é NÃO. O padrão de quem apertou Enter sem ler não pode ser
538
+ * entregar o mouse e o teclado.
539
+ */
540
+ async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
541
+ if (primeira) {
542
+ console.log('');
543
+ console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white('O Primo quer usar o seu computador.')));
544
+ console.log(' ' + c.muted('Ele vai mexer no mouse e no teclado de verdade, no programa que'));
545
+ console.log(' ' + c.muted('estiver na frente. Você vê cada passo aqui enquanto acontece.'));
546
+ }
547
+ console.log('');
548
+ console.log(' ' + c.brand('●') + ' ' + c.white(oQueVaiFazer));
549
+ console.log(' ' + c.dim('1') + c.muted(' agora ')
550
+ + c.dim('2') + c.muted(' esta sessão inteira ')
551
+ + c.dim('Enter') + c.muted(' não'));
552
+
553
+ const dito = String(await ask(' ' + c.brand('❯ ') + c.muted('1, 2 ou Enter '))).trim().toLowerCase();
554
+ if (dito === '1' || /^(s|sim|ok|pode|agora|y|yes)$/.test(dito)) {
555
+ console.log(' ' + c.ok('✔ autorizado') + c.dim(' — só esta ação'));
556
+ return 'agora';
557
+ }
558
+ if (dito === '2' || /sess|sempre|tudo|todas/.test(dito)) {
559
+ console.log(' ' + c.ok('✔ autorizado') + c.dim(' — esta sessão inteira. Fechar o PrimoCode esquece.'));
560
+ return 'sessao';
561
+ }
562
+ console.log(' ' + c.muted('Não autorizado.') + c.dim(' Ele vai resolver por outro caminho.'));
563
+ return 'nao';
564
+ }
565
+
566
+ /* ── A TELA DO MARKETPLACE ───────────────────────────────────────────────
567
+ * O desenho e as teclas vivem em lib/marketplace.js, que é função pura. O que
568
+ * mora aqui é o que precisa do terminal: tirar a caixa de digitar da frente,
569
+ * escutar as setas, e fazer o que a tela pediu (ligar uma skill, abrir o
570
+ * diálogo de criar).
571
+ *
572
+ * A divisão é a mesma do /effort, e pelo mesmo motivo: dois desenhos no mesmo
573
+ * lugar brigam pelo teclado e pelo cursor.
574
+ */
575
+ function abrirMarketplace() {
576
+ return new Promise((resolve) => {
577
+ if (!process.stdin.isTTY) {
578
+ // Sem terminal não há seta para apertar. Cair no /skills é melhor
579
+ // que desenhar uma tela que ninguém pode usar.
580
+ return handleSlash('/skills').then(resolve);
581
+ }
582
+
583
+ let estado = marketplace.novo(skills.listar());
584
+ const altura = () => Math.max(6, Math.min(16, (process.stdout.rows || 24) - 10));
585
+ let alturaDesenhada = 0;
586
+
587
+ const pintar = (primeira) => {
588
+ const texto = marketplace.montar(estado, {
589
+ colunas: process.stdout.columns || 80, altura: altura(),
590
+ });
591
+ const linhas = texto.split('\n');
592
+ // Redesenha POR CIMA do quadro anterior em vez de rolar a tela:
593
+ // sem isso, cada seta empurraria a tela inteira para cima e o
594
+ // terminal viraria um rolo de cópias do marketplace.
595
+ if (!primeira && alturaDesenhada) process.stdout.write(`\x1b[${alturaDesenhada}A\r`);
596
+ process.stdout.write(linhas.map((l) => '\x1b[2K' + l).join('\n') + '\n');
597
+ // O quadro novo pode ser mais curto que o anterior (a descrição
598
+ // que some ao mudar de item): o resto é apagado, senão sobra
599
+ // lixo da linha antiga embaixo.
600
+ if (alturaDesenhada > linhas.length) {
601
+ const sobra = alturaDesenhada - linhas.length;
602
+ process.stdout.write('\x1b[2K\n'.repeat(sobra) + `\x1b[${sobra}A`);
603
+ }
604
+ alturaDesenhada = Math.max(linhas.length, alturaDesenhada);
605
+ };
606
+
607
+ const aoTeclar = (ch, key) => {
608
+ if (!key) return;
609
+ const { estado: novoEstado, acao } = marketplace.andar(estado, key, { altura: altura() });
610
+ estado = novoEstado;
611
+
612
+ if (!acao) { pintar(false); return; }
613
+
614
+ if (acao.tipo === 'fechar') { encerrar(); resolve(); return; }
615
+
616
+ if (acao.tipo === 'alternar') {
617
+ skills.ligar(acao.skill.nome, !acao.skill.ligada);
618
+ // A lista é relida do disco: a skill muda de seção, e a tela
619
+ // tem de mostrar isso na hora — senão parece que nada
620
+ // aconteceu e a pessoa aperta Enter de novo.
621
+ estado = { ...estado, skills: skills.listar() };
622
+ pintar(false);
623
+ return;
624
+ }
625
+
626
+ if (acao.tipo === 'nova' || acao.tipo === 'instalar') {
627
+ encerrar();
628
+ (acao.tipo === 'nova' ? criarSkill() : instalarSkill())
629
+ .then(() => abrirMarketplace())
630
+ .then(resolve);
631
+ }
632
+ };
633
+
634
+ /* Largar o teclado e devolver a caixa. Chamado por TODAS as saídas —
635
+ o Esc, o "New Skill" e o "Instalar" —, porque o diálogo que vem
636
+ depois usa o `ask`, e o `ask` precisa da caixa de volta. */
637
+ const encerrar = () => {
638
+ process.stdin.removeListener('keypress', aoTeclar);
639
+ process.stdin.removeListener('end', aoAcabar);
640
+ process.stdin.removeListener('close', aoAcabar);
641
+ sliderAtivo = false;
642
+ if (rl && rl.retomar) rl.retomar();
643
+ };
644
+
645
+ /* A entrada pode acabar com a tela aberta (pipe que terminou, Ctrl+D,
646
+ terminal fechado). Sem isto o programa ficaria esperando para
647
+ sempre uma tecla que não vem mais — pendurado e calado, que é o
648
+ pior jeito de travar. */
649
+ const aoAcabar = () => { encerrar(); resolve(); };
650
+ process.stdin.once('end', aoAcabar);
651
+ process.stdin.once('close', aoAcabar);
652
+
653
+ sliderAtivo = true;
654
+ if (rl && rl.suspender) rl.suspender();
655
+ pintar(true);
656
+ process.stdin.on('keypress', aoTeclar);
657
+ });
658
+ }
659
+
660
+ /* ── New Skill: o nome, e depois o prompt ────────────────────────────────
661
+ * Em sequência, como foi pedido. O conteúdo é multilinha e termina numa linha
662
+ * com apenas `.` — não em Enter: o prompt de uma skill tem parágrafos, e
663
+ * terminar no primeiro Enter tornaria impossível escrever uma skill de
664
+ * verdade sem colar tudo numa linha só.
665
+ */
666
+ async function criarSkill() {
667
+ console.log('');
668
+ console.log(' ' + c.bold(c.brand('New Skill')));
669
+ console.log(' ' + c.muted('Uma skill é conhecimento em texto: o que o Primo deve saber'));
670
+ console.log(' ' + c.muted('e seguir quando o assunto aparecer.'));
671
+ console.log('');
672
+
673
+ const nome = String(await ask(' ' + c.brand('❯ ') + c.white('Nome da skill: '))).trim();
674
+ if (!nome) { console.log(c.muted(' Sem nome, sem skill.')); return; }
675
+
676
+ console.log('');
677
+ console.log(' ' + c.muted('Agora o conteúdo — o prompt inteiro dela.'));
678
+ console.log(' ' + c.dim('Escreva quantas linhas quiser. Termine com um ponto sozinho numa linha.'));
679
+ console.log('');
680
+
681
+ const linhas = [];
682
+ for (;;) {
683
+ const l = await ask(' ' + c.dim('│ '));
684
+ if (String(l).trim() === '.') break;
685
+ linhas.push(l);
686
+ // Sem teto, um Enter preso na tecla viraria um arquivo de milhões de
687
+ // linhas. 400 é muito mais do que qualquer skill precisa.
688
+ if (linhas.length >= 400) { console.log(c.muted(' (chegou ao limite de linhas)')); break; }
689
+ }
690
+ const corpo = linhas.join('\n').trim();
691
+ if (!corpo) { console.log(c.muted(' Sem conteúdo, sem skill.')); return; }
692
+
693
+ console.log('');
694
+ const quando = String(await ask(' ' + c.brand('❯ ') + c.white('Quando usar? ')
695
+ + c.dim('(palavras separadas por vírgula) '))).trim();
696
+ const descricao = String(await ask(' ' + c.brand('❯ ') + c.white('Uma linha do que ela faz: '))).trim();
697
+
698
+ const r = skills.criar(nome, corpo, { quando, descricao });
699
+ if (!r.ok) { console.log(c.err(` ${r.error}`)); return; }
700
+ console.log('');
701
+ console.log(c.ok(` Skill cadastrada: ${r.nome}`) + c.muted(` (${shortPath(r.caminho)})`));
702
+ if (!quando) {
703
+ // Sem gatilho ela nunca entra sozinha num pedido. Dizer isso agora
704
+ // evita a conclusão de que "a skill não funciona".
705
+ console.log(c.warn(' Sem palavras em "quando usar", ela não vai entrar sozinha nos pedidos.'));
706
+ console.log(c.dim(' Edite o arquivo e preencha o campo `quando:` quando quiser.'));
707
+ }
708
+ }
709
+
710
+ async function instalarSkill() {
711
+ console.log('');
712
+ const caminho = String(await ask(' ' + c.brand('❯ ') + c.white('Caminho do SKILL.md: '))).trim();
713
+ if (!caminho) return;
714
+ const r = skills.instalarDe(caminho);
715
+ console.log(r.ok ? c.ok(` Skill instalada: ${r.nome}`) : c.err(` ${r.error}`));
716
+ }
717
+
548
718
  async function sendPrompt(userText) {
549
719
  /* O portão do plano. Sem ele o bloqueio da abertura seria só um texto na
550
720
  tela: a pessoa leria "você não tem acesso" e mandaria a primeira coisa
@@ -607,6 +777,11 @@ async function sendPrompt(userText) {
607
777
 
608
778
  let r = null;
609
779
  let jaFeito = '';
780
+ /* O livro do PEDIDO, não do passo. Um plano de três passos são três
781
+ chamadas de runAct, cada uma com o seu registro — e o balanço que
782
+ interessa é o do pedido inteiro: quem pediu um vídeo não quer três
783
+ balanços, quer saber o que existe no disco quando ele terminar. */
784
+ const livro = registro.novo();
610
785
  for (let i = 0; i < passos.length; i++) {
611
786
  if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
612
787
  // Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
@@ -634,7 +809,10 @@ async function sendPrompt(userText) {
634
809
  // Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
635
810
  // não é motivo para criar pasta de projeto.
636
811
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
637
- motor: motorDaVez(),
812
+ // O portão do computador: quem pergunta é o terminal, quem
813
+ // lembra da resposta é a sessão.
814
+ autorizar: autorizarControle,
815
+ controle: state.controle,
638
816
  });
639
817
 
640
818
  r = await rodarPasso();
@@ -646,11 +824,18 @@ async function sendPrompt(userText) {
646
824
  console.log(c.dim(' ⎿ o passo não concluiu — tentando de novo'));
647
825
  r = await rodarPasso();
648
826
  }
827
+ registro.juntar(livro, r.registro);
649
828
  jaFeito = (r.concluiu !== false)
650
829
  ? String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200)
651
830
  : '';
652
831
  if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
653
832
  }
833
+
834
+ /* O balanço, depois da resposta. Depois, e não antes, de propósito: o
835
+ que a pessoa quer ler primeiro é o que ele TEM A DIZER; a lista de
836
+ arquivos é a conferência, e conferência se lê por último. */
837
+ const balanco = registro.resumo(livro, { colunas: process.stdout.columns || 80 });
838
+ if (balanco) console.log(balanco);
654
839
  state.history.push({ role: 'user', content: userText });
655
840
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
656
841
  state.lastResponse = r.assistantMessage.content;
@@ -695,30 +880,39 @@ async function sendPrompt(userText) {
695
880
  const RE_DESCOBRIR = /\b(pesquis|busca|procur|descubr|investig|analis|leia|estud|olh[ae] (o site|a p[áa]gina)|ver o que)\w*/i;
696
881
  const RE_CRIAR_DEPOIS = /\b(cri[ae]|faz|faça|mont[ae]|ger[ae]|escrev[ae]|produz|desenvolv)\w*/i;
697
882
 
698
- /** "baixa/exporta o vídeo" resolvido na hora, sem gastar modelo. */
699
- // "me manda o vídeo", "quero pra mandar no zap", "gera o arquivo" — a lista
700
- // era curta e o pedido sai de muitos jeitos. Fora dela, o pedido virava
701
- // tarefa de agente: gasto de API para abrir uma URL que o CLI já sabe.
702
- const RE_BAIXAR = /\b(baixa\w*|exporta\w*|download|salva\w*|grava\w*|me manda|manda(r)? (o|a|esse|essa)|gera(r)? o arquivo|em mp4|pra mandar|arquivo do)\b/i;
703
- const RE_PECA_STUDIO = /\b(v[ií]deo|mp4|mp3|apresenta[çc][ãa]o|slides?|reels?|anima[çc][ãa]o|filme)\b/i;
883
+ /* "me manda o vídeo" resolvido na hora, sem gastar modelo.
884
+ *
885
+ * Antes isso abria a última criação do Studio no navegador com `#gravar`, e a
886
+ * pessoa precisava aceitar uma permissão de tela para o navegador GRAVAR a
887
+ * animação passando — era assim que o mp4 nascia.
888
+ *
889
+ * Isso acabou junto com o renderizador de navegador: o Remotion escreve o
890
+ * arquivo direto, e todo render já deixa um .mp4 em `saida/`. Então "baixa o
891
+ * vídeo" não é mais uma tarefa — é dizer onde ele está, que o CLI sabe sem
892
+ * perguntar a modelo nenhum.
893
+ */
894
+ const RE_BAIXAR = /\b(baixa\w*|exporta\w*|download|salva\w*|me manda|manda(r)? (o|a|esse|essa)|gera(r)? o arquivo|em mp4|pra mandar|arquivo do|onde (est[áa]|ficou))\b/i;
895
+ const RE_PECA_STUDIO = /\b(v[ií]deo|mp4|apresenta[çc][ãa]o|slides?|reels?|anima[çc][ãa]o|filme)\b/i;
704
896
 
705
897
  async function baixarCriacaoDireto(texto) {
706
898
  if (!(RE_BAIXAR.test(texto) && RE_PECA_STUDIO.test(texto))) return false;
707
899
  try {
708
- const studio = require('../lib/studio');
709
- const r = await studio.listar();
710
- const lista = (r && r.arquivos) || [];
711
- // a criação mais nova que é vídeo; sem vídeo, a mais nova de todas
712
- const alvo = lista.find((a) => a.ferramenta === 'corte') || lista[0];
713
- if (!alvo) return false;
714
- const url = `http://localhost:7777/v/${alvo.id}#gravar`;
715
- await tools.execute('open_in_browser', { target: url }, { projectDir: state.workDir || os.homedir() });
716
- console.log(c.ok(` ⎿ abrindo "${alvo.titulo}" para gravar`));
717
- console.log(c.muted(' O navegador vai pedir permissão de tela — aceite (marque "compartilhar áudio da aba"'));
718
- console.log(c.muted(' para levar a narração junto). O vídeo roda sozinho e baixa no fim.'));
900
+ const { projeto } = require('../lib/estudio');
901
+ // O mais recente que já foi renderizado: quem diz "me manda o vídeo"
902
+ // fala do último, e um projeto sem render não tem o que mandar.
903
+ const comArquivo = projeto.listar({ limite: 30 })
904
+ .find((p) => (p.renders || []).some((r) => fs.existsSync(r.arquivo)));
905
+ if (!comArquivo) return false;
906
+ const ultimo = [...comArquivo.renders].reverse().find((r) => fs.existsSync(r.arquivo));
907
+
908
+ await tools.execute('open_in_browser', { target: ultimo.arquivo },
909
+ { projectDir: state.workDir || os.homedir() });
910
+ console.log(c.ok(` ⎿ abrindo "${comArquivo.titulo}"`));
911
+ console.log(c.muted(' ') + c.white(ultimo.arquivo));
912
+ console.log(c.dim(` ${(ultimo.bytes / 1048576).toFixed(1)} MB · o arquivo já estava pronto, é só copiar daí.`));
719
913
  return true;
720
914
  } catch {
721
- return false; // Studio fora do ar ou sem criações: segue o caminho normal
915
+ return false; // sem projeto renderizado: segue o caminho normal
722
916
  }
723
917
  }
724
918
 
@@ -827,17 +1021,6 @@ async function runChat(promptForModel, userTextForMemory) {
827
1021
 
828
1022
  // ── Slash commands ─────────────────────────────────────────────────────
829
1023
 
830
- /* Qual motor executa este turno.
831
- *
832
- * Um `if` a mais aqui e um `if` a menos em qualquer outro lugar seria o
833
- * caminho para o /codex funcionar no modo agente e não no chat, ou ao
834
- * contrário. Um lugar só decide, e os dois modos chamam este. */
835
- function motorDaVez() {
836
- if (state.engine === 'claude-code') return claudeEngine;
837
- if (state.engine === 'codex') return codexEngine;
838
- return api;
839
- }
840
-
841
1024
  function isSlash(text) { return text.trim().startsWith('/'); }
842
1025
 
843
1026
  /* ── OS NOMES ANTIGOS ────────────────────────────────────────────────────
@@ -861,8 +1044,6 @@ const RENOMEADOS = {
861
1044
  entrar: 'connect', login: 'connect', conectar: 'connect',
862
1045
  conta: 'account',
863
1046
  sair: 'exit', // sair é SAIR; desconectar a conta é /logout
864
- // as chaves de API
865
- chave: 'key', chaves: 'key',
866
1047
  // o histórico
867
1048
  conversas: 'chats', historico: 'chats', histórico: 'chats',
868
1049
  // o resto
@@ -974,97 +1155,6 @@ async function handleSlash(text) {
974
1155
  break;
975
1156
  }
976
1157
 
977
- /* A chave de API da própria pessoa. Guardada em ~/.primocode/
978
- chaves.json com permissão 600 — nunca no config.json, que é o
979
- arquivo que se cola num chamado de suporte. */
980
- case 'key': case 'keys': {
981
- const [qual, ...resto] = rest.split(/\s+/).filter(Boolean);
982
- const valor = resto.join(' ');
983
-
984
- if (!qual) {
985
- console.log(c.white('\n Suas chaves de API'));
986
- for (const p of chaves.listar()) {
987
- console.log(' ' + c.white(p.nome.padEnd(8))
988
- + (p.tem ? c.ok(p.disfarce) : c.dim('não configurada'))
989
- + c.dim(` ${p.variavel}`));
990
- }
991
- console.log(c.dim('\n /key claude sk-ant-… guarda a chave'));
992
- console.log(c.dim(' /key gpt remover apaga a que estiver lá'));
993
- console.log(c.muted('\n Sem chave própria, cada motor usa a conta em que você já entrou nele.'));
994
- break;
995
- }
996
- if (!chaves.PROVEDORES[qual]) {
997
- console.log(c.err(` Não conheço "${qual}". Use: ${Object.keys(chaves.PROVEDORES).join(', ')}`));
998
- break;
999
- }
1000
- if (valor === 'remover' || valor === 'apagar') {
1001
- const r = chaves.esquecer(qual);
1002
- console.log(r.ok ? c.ok(` Chave do ${chaves.PROVEDORES[qual].nome} apagada.`)
1003
- : c.muted(` ${r.error}`));
1004
- break;
1005
- }
1006
- if (!valor) {
1007
- const p = chaves.PROVEDORES[qual];
1008
- console.log(c.muted(` Uso: `) + c.brand(`/key ${qual} ${p.prefixo}…`));
1009
- console.log(c.dim(` Pegue a sua em ${p.onde}`));
1010
- break;
1011
- }
1012
- const r = chaves.guardar(qual, valor);
1013
- if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
1014
- // O que se mostra de volta é o disfarce, nunca a chave: a linha
1015
- // fica no scrollback do terminal e no print que a pessoa manda.
1016
- console.log(c.ok(` Chave do ${r.nome} guardada.`) + c.muted(` ${r.disfarce}`));
1017
- console.log(c.dim(` Ela vai como ${chaves.PROVEDORES[qual].variavel} só quando o motor rodar.`));
1018
- break;
1019
- }
1020
-
1021
- /* O Codex da conta da pessoa, com o modelo barato.
1022
- O protocolo é o MESMO do /claude — ver lib/codex-engine.js. */
1023
- /* `/gpt` e `/codex` são a mesma coisa e existem os dois de propósito:
1024
- o programa se chama Codex, mas quem usa fala "GPT" — e o comando que
1025
- a pessoa tenta é o nome que ela tem na cabeça, não o do fabricante. */
1026
- case 'gpt': case 'codex': {
1027
- if (rest.toLowerCase() === 'off') {
1028
- if (state.engine !== 'codex') { console.log(c.muted(' Já estava no motor padrão.')); break; }
1029
- state.engine = 'server';
1030
- console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
1031
- break;
1032
- }
1033
- console.log(c.dim(' Checando o Codex…'));
1034
- const r = codexEngine.garantirEngine();
1035
- if (!r.ok) {
1036
- console.log(c.err(` ${r.error}`));
1037
- if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
1038
- break;
1039
- }
1040
- state.engine = 'codex';
1041
- console.log(c.ok(` Motor: Codex (${r.modelo})`) + c.muted(' — na sua conta, não na minha'));
1042
- console.log(c.muted(' /codex off volta pro motor padrão.'));
1043
- break;
1044
- }
1045
-
1046
- case 'claude': {
1047
- if (rest.toLowerCase() === 'off') {
1048
- if (state.engine !== 'claude-code') { console.log(c.muted(' Já estava no motor padrão.')); break; }
1049
- state.engine = 'server';
1050
- console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
1051
- break;
1052
- }
1053
- console.log(c.dim(' Checando Claude Code e ORCA…'));
1054
- const r = await claudeEngine.garantirEngine();
1055
- if (!r.ok) {
1056
- console.log(c.err(` ${r.error}`));
1057
- if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
1058
- break;
1059
- }
1060
- state.engine = 'claude-code';
1061
- const onde = r.caminho === 'A' ? 'sessão contínua via ORCA' : 'chamada avulsa (claude -p, sem ORCA)';
1062
- console.log(c.ok(` Motor: Claude Code (${onde})`)
1063
- + c.muted(` — ${claudeEngine.MODELO_CLAUDE || 'o padrão da sua conta'}, na sua conta`));
1064
- console.log(c.muted(' /claude off volta pro motor padrão.'));
1065
- break;
1066
- }
1067
-
1068
1158
  // O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
1069
1159
  // Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
1070
1160
  // ("/effort high") aplica direto, para quem já sabe o que quer.
@@ -1264,6 +1354,191 @@ async function handleSlash(text) {
1264
1354
  break;
1265
1355
  }
1266
1356
 
1357
+ /* ── AS SKILLS ───────────────────────────────────────────────────
1358
+ * `/skills` lista, `/install-skill` traz de um arquivo, e
1359
+ * `/marketplace` é a tela onde se navega e liga.
1360
+ *
1361
+ * Nenhum dos três pede confirmação para ligar ou desligar: skill é
1362
+ * TEXTO, e o pior que um texto ruim faz é dar um conselho ruim ao
1363
+ * modelo — que o usuário vê acontecendo, ação por ação, na tela.
1364
+ */
1365
+ case 'skills': {
1366
+ const lista = skills.listar();
1367
+ if (!lista.length) { console.log(c.muted(' Nenhuma skill instalada.')); break; }
1368
+ const ligadas = lista.filter((s) => s.ligada);
1369
+ console.log('');
1370
+ console.log(c.bold(c.white(' Skills')) + c.dim(` ${ligadas.length} de ${lista.length} ligadas`));
1371
+ console.log('');
1372
+ for (const s of lista) {
1373
+ const marca = s.ligada ? c.ok('●') : c.dim('○');
1374
+ console.log(` ${marca} ${c.white(ui.padRight(s.titulo, 28))}`
1375
+ + c.dim(ui.truncate(s.descricao, (process.stdout.columns || 80) - 36)));
1376
+ }
1377
+ console.log('');
1378
+ console.log(c.dim(' /marketplace liga e desliga · o corpo da skill só entra no pedido'));
1379
+ console.log(c.dim(' quando o assunto bate, para não gastar o teto de tokens à toa.'));
1380
+ break;
1381
+ }
1382
+
1383
+ case 'install-skill': case 'installskill': {
1384
+ if (!rest) {
1385
+ console.log(c.muted(' Uso: ') + c.brand('/install-skill <caminho do SKILL.md>'));
1386
+ console.log(c.dim(' Ou abra o ') + c.brand('/marketplace') + c.dim(' e escolha "New Skill" para escrever uma.'));
1387
+ break;
1388
+ }
1389
+ const r = skills.instalarDe(rest);
1390
+ if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
1391
+ console.log(c.ok(` Skill instalada: ${r.nome}`) + c.muted(` (${shortPath(r.caminho)})`));
1392
+ console.log(c.dim(' Já está ligada. /skills mostra todas.'));
1393
+ break;
1394
+ }
1395
+
1396
+ /* ── /voice ──────────────────────────────────────────────────────
1397
+ * O modo voz abre uma janelinha com uma orb — não é uma janela de
1398
+ * terminal. O aplicativo é Python (ver voz/primo_voz); o que acontece
1399
+ * aqui é subir ele e responder ao que ele pedir.
1400
+ *
1401
+ * O cérebro NÃO vai junto: cada frase transcrita volta para o mesmo
1402
+ * `runAct` do terminal, com as mesmas ferramentas, as mesmas skills e
1403
+ * a mesma permissão de computador. Portar o laço do agente para o
1404
+ * Python seria manter dois laços, e dois laços do mesmo agente é a
1405
+ * garantia de que um deles está errado.
1406
+ */
1407
+ case 'voice': case 'voz': {
1408
+ const pronto = vozModo.conferir();
1409
+ if (!pronto.ok && !pronto.instalavel) {
1410
+ console.log(c.err(` ${pronto.error}`));
1411
+ if (pronto.comoResolver) console.log(c.muted(` ${pronto.comoResolver}`));
1412
+ break;
1413
+ }
1414
+
1415
+ if (!pronto.ok) {
1416
+ console.log(c.muted(` ${pronto.error}`));
1417
+ if (!(await confirm('Instalar agora?'))) {
1418
+ console.log(c.muted(' Sem eles o modo voz não abre.'));
1419
+ if (pronto.comoResolver) console.log(c.dim(` ${pronto.comoResolver}`));
1420
+ break;
1421
+ }
1422
+ const sp = new Spinner('instalando').start();
1423
+ const r = await vozModo.instalar(pronto.python, pronto.instalavel, () => {});
1424
+ sp.stop();
1425
+ if (!r.ok) {
1426
+ console.log(c.err(` não deu: ${r.error}`));
1427
+ console.log(c.muted(` ${pronto.comoResolver}`));
1428
+ break;
1429
+ }
1430
+ const conferido = vozModo.conferir();
1431
+ if (!conferido.ok) {
1432
+ console.log(c.err(` ainda falta: ${conferido.error}`));
1433
+ if (conferido.comoResolver) console.log(c.muted(` ${conferido.comoResolver}`));
1434
+ break;
1435
+ }
1436
+ }
1437
+
1438
+ const py = (pronto.ok ? pronto : vozModo.conferir()).python;
1439
+ console.log('');
1440
+ console.log(' ' + c.bold(c.brand('Primo Code Voice')));
1441
+ console.log(' ' + c.muted('É só falar. A orb responde: ')
1442
+ + c.dim('parada espera · pulsa ouvindo · respira pensando · brilha falando'));
1443
+ console.log(' ' + c.dim('Falar por cima corta a resposta · Esc ou fechar a janela encerra'));
1444
+ console.log('');
1445
+
1446
+ // A caixa de digitar sai da frente: quem está no modo voz não
1447
+ // está digitando, e uma caixa piscando embaixo é ruído.
1448
+ if (rl && rl.suspender) rl.suspender();
1449
+ sliderAtivo = true;
1450
+
1451
+ const r = await vozModo.abrir({
1452
+ python: py,
1453
+ servidor: state.config.server,
1454
+ token: state.config.token,
1455
+ voz: state.config.voz || 'antonio',
1456
+ mostrar: (texto, deErro) => {
1457
+ if (deErro) console.log(' ' + c.dim(texto));
1458
+ else console.log(' ' + c.brand('❯ ') + c.white(texto));
1459
+ },
1460
+ /* O agente de verdade. A instrução de FORMA vai na frente
1461
+ porque resposta boa de ler é ruim de ouvir — o trabalho é o
1462
+ mesmo, o jeito de contar é que muda. */
1463
+ pensar: async (texto) => {
1464
+ const plan = prepararPasta(texto);
1465
+ const resultado = await runAct(`${vozModo.COMO_FALAR}\n\nPEDIDO: ${texto}`, {
1466
+ server: state.config.server,
1467
+ model: state.config.model,
1468
+ effort: state.config.effort,
1469
+ token: state.config.token,
1470
+ history: memoria.paraEnviar(state.memoria, state.history),
1471
+ projectDir: plan.dir,
1472
+ projectName: plan.name || state.currentProject,
1473
+ ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
1474
+ autorizar: autorizarControle,
1475
+ controle: state.controle,
1476
+ });
1477
+ const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
1478
+ // A conversa é a MESMA do terminal: sair do modo voz e
1479
+ // continuar digitando não pode começar do zero.
1480
+ state.history.push({ role: 'user', content: texto });
1481
+ state.history.push({ role: 'assistant', content: resposta });
1482
+ const balanco = registro.resumo(resultado.registro, { colunas: process.stdout.columns || 80 });
1483
+ if (balanco) console.log(balanco);
1484
+ return resposta;
1485
+ },
1486
+ });
1487
+
1488
+ sliderAtivo = false;
1489
+ if (rl && rl.retomar) rl.retomar();
1490
+ if (!r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
1491
+ else console.log(c.muted(' Modo voz encerrado.'));
1492
+ break;
1493
+ }
1494
+
1495
+ case 'marketplace': case 'market': {
1496
+ await abrirMarketplace();
1497
+ break;
1498
+ }
1499
+
1500
+ /* ── /connectors ─────────────────────────────────────────────────
1501
+ * Só mostra. Não há o que configurar — os conectores vivem no
1502
+ * servidor, com as chaves dele, e é assim de propósito: a promessa do
1503
+ * produto é que a API já vem pronta.
1504
+ *
1505
+ * Então por que existe? Porque "a IA está lenta hoje" e "ele errou o
1506
+ * código" têm causas diferentes e invisíveis: qual conector atendeu.
1507
+ * Sem esta tela, a única resposta possível é dar de ombros. Com ela,
1508
+ * dá para ver que o preferido está fora e outro assumiu — e essa é a
1509
+ * primeira pergunta de todo suporte.
1510
+ */
1511
+ case 'connectors': case 'conectores': {
1512
+ const sp = new Spinner('lendo os conectores').start();
1513
+ let dados = null;
1514
+ try {
1515
+ dados = await api.getConectores(state.config.server, state.config.token);
1516
+ } catch (e) { sp.stop(); console.log(c.err(` Não deu para ler: ${ui.friendlyError(e)}`)); break; }
1517
+ sp.stop();
1518
+
1519
+ const lista = (dados && dados.conectores) || [];
1520
+ if (!lista.length) { console.log(c.muted(' O servidor não listou conector nenhum.')); break; }
1521
+
1522
+ console.log('');
1523
+ console.log(c.bold(c.white(' Conectores')) + c.dim(' — quem atende os seus pedidos'));
1524
+ console.log('');
1525
+ for (const cx of lista) {
1526
+ const marca = cx.ligado ? c.ok('●') : c.dim('○');
1527
+ const selos = [
1528
+ cx.gratis ? c.dim('grátis') : c.warn('pago'),
1529
+ cx.faz && cx.faz.ferramentas ? c.dim('ferramentas') : c.dim('só conversa'),
1530
+ ].join(c.dim(' · '));
1531
+ console.log(` ${marca} ${c.white(ui.padRight(cx.nome, 24))}${selos}`);
1532
+ console.log(` ${c.dim(ui.truncate(cx.porque || '', (process.stdout.columns || 80) - 6))}`);
1533
+ }
1534
+ const dePe = lista.filter((x) => x.ligado).length;
1535
+ console.log('');
1536
+ console.log(c.muted(` ${dePe} de ${lista.length} de pé.`)
1537
+ + c.dim(' Quem está fora não some da fila: entra assim que o servidor tiver a chave.'));
1538
+ console.log(c.dim(' Não há nada para você configurar — a API vem com o PrimoCode.'));
1539
+ break;
1540
+ }
1541
+
1267
1542
  /* A janela. "Se mandar /rc pra ele, ele abre o aplicativo — para o
1268
1543
  Mac e o Windows, já instalado junto com o npm, com as cores do
1269
1544
  PrimoCode. Um chat estilo terminal para quem não quer o terminal."
@@ -1343,11 +1618,6 @@ async function handleSlash(text) {
1343
1618
  console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.uid.slice(0, 12) + '…')
1344
1619
  + c.muted(` · plano ${jaEsta.plano}`));
1345
1620
  console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/connect trocar'));
1346
- // Sem isto, quem tinha sido barrado na abertura (plano velho,
1347
- // login que não deu a tempo) e depois só confere com um
1348
- // `/connect` continuava barrado: este comando não passava por
1349
- // `entrar()`, e nada reabria o portão.
1350
- sincronizarLiberacao(jaEsta);
1351
1621
  break;
1352
1622
  }
1353
1623
  if (rest === 'trocar') conta.sair();
@@ -1357,29 +1627,31 @@ async function handleSlash(text) {
1357
1627
  abrir: (u) => tools.openInBrowser(u),
1358
1628
  contar: (info) => {
1359
1629
  sp.stop();
1360
- /* O CÓDIGO NÃO APARECE MAIS NO TERMINAL.
1361
- *
1362
- * "Pode tirar essa parte no login onde aparece o código
1363
- * — o terminal mandou o código, pode tirar."
1364
- *
1365
- * A página do token já resolve o pareamento sozinha: ela
1366
- * pergunta o código ao 127.0.0.1 desta máquina, e onde o
1367
- * navegador bloqueia isso (Safari) ela oferece "Colar" —
1368
- * o código continua indo para a área de transferência,
1369
- * que é o caminho sem leitura. Oito letras impressas aqui
1370
- * eram uma tarefa para a pessoa que o produto já não
1371
- * precisa, e a tela ficava dizendo que talvez precisasse.
1372
- *
1373
- * O código continua existindo no protocolo (lib/conta.js)
1374
- * — some da TELA, não do pareamento. */
1630
+ const meio = info.codigo.length === 8
1631
+ ? info.codigo.slice(0, 4) + '-' + info.codigo.slice(4) : info.codigo;
1375
1632
  console.log(c.muted(' Abrindo no navegador:'));
1376
1633
  console.log(' ' + c.brand(info.url.replace(/[?&]porta=\d+/, '')));
1377
1634
  console.log('');
1378
- console.log(' ' + c.white('Entre com a sua conta na página que abriu — o resto é automático.'));
1379
- if (!info.porta && info.copiado) {
1380
- // Sem a porta local, quem fecha o pareamento é a área de
1381
- // transferência: a página pede um toque em "Colar".
1382
- console.log(c.dim(' Se a página pedir para colar, é só colar — já está na sua área de transferência.'));
1635
+ if (info.porta) {
1636
+ /* COM O SERVIDOR LOCAL DE PÉ, NÃO HÁ CÓDIGO PARA LER. A
1637
+ página pergunta ao 127.0.0.1 desta máquina, recebe o
1638
+ código e aprova sozinha assim que a pessoa entra com a
1639
+ conta — "não precisa colocar código, não precisa pra
1640
+ nada, porque ele já libera". Mostrar as oito letras
1641
+ grandes aqui era pedir para ler o que ninguém precisa
1642
+ mais; elas ficam numa linha apagada, só para o caso
1643
+ de o navegador bloquear o 127.0.0.1 (Safari) e a
1644
+ página voltar a pedir. */
1645
+ console.log(' ' + c.white('Entre com a sua conta na página que abriu — o resto é automático.'));
1646
+ console.log(c.dim(` Se a página pedir um código: ${meio}${info.copiado ? ' (já copiado)' : ''}.`));
1647
+ } else {
1648
+ /* Sem porta local (ocupadas, ou bloqueadas), o código
1649
+ volta a ser o caminho — grande, partido em dois,
1650
+ porque é o que se lê de uma tela e se confere noutra. */
1651
+ console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
1652
+ console.log('');
1653
+ console.log(c.dim(' ' + (info.copiado ? 'O código já está copiado — cole na página.'
1654
+ : 'Digite o código na página.')));
1383
1655
  }
1384
1656
  console.log(c.dim(' Esperando o navegador…'));
1385
1657
  },
@@ -1388,11 +1660,6 @@ async function handleSlash(text) {
1388
1660
  if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
1389
1661
  console.log(c.ok(' Conectado.')
1390
1662
  + c.muted(` Plano ${r.plano}${r.pro ? '' : ' — assine Premium no app para liberar tudo'}`));
1391
- // O QUE FALTAVA: sem isto, "Conectado. Plano premium" na tela e o
1392
- // PRÓXIMO pedido caindo em "O PrimoCode pede Premium ou Super" —
1393
- // porque o portão (`state.liberado`) só era decidido na abertura,
1394
- // e um /connect no meio da sessão nunca o reabria.
1395
- sincronizarLiberacao(r);
1396
1663
  break;
1397
1664
  }
1398
1665
 
@@ -1450,10 +1717,6 @@ async function handleSlash(text) {
1450
1717
  conta.sair();
1451
1718
  console.log(c.ok(' Desconectado deste computador.')
1452
1719
  + c.dim(' A conta continua intacta; entre de novo com /connect.'));
1453
- // O mesmo portão do connect: sem login não há como confirmar
1454
- // Premium/Super, e deixar `state.liberado` de pé seria a mesma
1455
- // dessincronia ao contrário — trabalhando sem estar conectado.
1456
- sincronizarLiberacao({ pro: false, motivo: 'sem-login' });
1457
1720
  break;
1458
1721
  }
1459
1722
 
@@ -1501,6 +1764,25 @@ async function handleSlash(text) {
1501
1764
  // rl.close() em vez de process.exit(): sair na marra descarta o que
1502
1765
  // ainda estava no buffer de saída.
1503
1766
  case 'exit': case 'quit': if (rl) rl.close(); break;
1767
+ /* ── OS COMANDOS APOSENTADOS ──────────────────────────────────────
1768
+ * `/claude`, `/gpt`, `/codex` e `/key` existiam quando o PrimoCode
1769
+ * rodava em cima da assinatura da própria pessoa: ela instalava o
1770
+ * Claude Code ou o Codex, colava uma chave de API, e só então o
1771
+ * agente pensava.
1772
+ *
1773
+ * Isso acabou. A API vem configurada no produto: quem entra na conta
1774
+ * já tem motor, e não há provedor nenhum para escolher.
1775
+ *
1776
+ * Eles não somem calados. Quem tem o dedo viciado em `/claude` e
1777
+ * bate num "comando não existe" conclui que a atualização quebrou
1778
+ * alguma coisa — e não vai procurar no /help, vai desinstalar. */
1779
+ case 'claude': case 'gpt': case 'codex': case 'key': case 'keys': {
1780
+ console.log(c.muted(' Não existe mais provedor para configurar.'));
1781
+ console.log(c.muted(' A API já vem com o PrimoCode: entrou na conta, pode usar.'));
1782
+ console.log(c.dim(' /effort escolhe o esforço · /account mostra o seu plano'));
1783
+ break;
1784
+ }
1785
+
1504
1786
  default: {
1505
1787
  // "/" sozinho abre o menu; um prefixo único autopreenche e executa
1506
1788
  // ("/eff" → /effort); prefixo ambíguo mostra as opções.
@@ -1645,7 +1927,11 @@ async function main() {
1645
1927
  userName: state.config.userName,
1646
1928
  pastaPadrao: state.workDirOrigem === 'padrao',
1647
1929
  }));
1648
- rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt(), completer: completarSlash });
1930
+ rl = entrada.criar({
1931
+ status: barraDeStatus,
1932
+ comandos: () => SLASH_COMANDOS,
1933
+ vazio: 'peça alguma coisa, ou / para os comandos',
1934
+ });
1649
1935
 
1650
1936
  rl.prompt();
1651
1937
 
@@ -1656,57 +1942,31 @@ async function main() {
1656
1942
  let closed = false;
1657
1943
  let saidBye = false;
1658
1944
 
1659
- // ── Sugestão fantasma: o autopreenchimento VISÍVEL enquanto digita ──
1660
- // "/str" mostra "eaming" esmaecido na frente do cursor, como o Claude
1661
- // Code. Seta → (ou Tab) aceita; Enter com o pedaço já executa. Desenhada
1662
- // DEPOIS de o readline processar a tecla (setImmediate), por cima do que
1663
- // sobrou da linha (\\x1b[0K limpa), e o cursor volta para o lugar.
1664
- if (process.stdin.isTTY && process.stdout.isTTY) {
1665
- let fantasma = '';
1666
- const desenharFantasma = () => {
1667
- if (busy || pendingAsk || sliderAtivo || !rl) return;
1668
- const linha = rl.line;
1669
- const noFim = rl.cursor === linha.length;
1670
- fantasma = '';
1671
- if (noFim && linha.startsWith('/') && !/\s/.test(linha) && linha.length > 1) {
1672
- const baixa = linha.toLowerCase();
1673
- const hit = SLASH_COMANDOS.map(([n]) => n).find((n) => n.startsWith(baixa) && n.length > linha.length);
1674
- if (hit) fantasma = hit.slice(linha.length);
1675
- }
1676
- if (!noFim) return; // cursor no meio: não mexe no que vem depois dele
1677
- // Limpa resto da linha (fantasma antigo) e desenha o novo, voltando o cursor.
1678
- process.stdout.write('\x1b[0K' + (fantasma ? `\x1b[2m${fantasma}\x1b[0m\x1b[${fantasma.length}D` : ''));
1679
- };
1680
- // ── Esc Esc: parar o agente no meio do trabalho ──
1681
- // Um Esc avisa; o segundo (em até 2s) manda parar. Dois Escs tão
1682
- // rápidos que chegam colados num chunk só viram UMA tecla com
1683
- // sequence "\x1b\x1b" — vale como os dois.
1684
- let ultimoEsc = 0;
1685
- process.stdin.on('keypress', (ch, key) => {
1686
- if (sliderAtivo) return; // o slider é dono do teclado enquanto está no ar
1687
- if (key && (key.name === 'escape' || key.sequence === '\x1b\x1b')) {
1688
- if (!busy) { ultimoEsc = 0; return; } // sem agente rodando, Esc é só Esc
1689
- const agora = Date.now();
1690
- if (key.sequence === '\x1b\x1b' || agora - ultimoEsc < 2000) {
1691
- ultimoEsc = 0;
1692
- interromper();
1693
- if (chatEmVoo) { try { chatEmVoo.abort(); } catch {} }
1694
- process.stdout.write('\n' + c.warn(' ⏹ parando o agente…') + '\n');
1695
- } else {
1696
- ultimoEsc = agora;
1697
- process.stdout.write('\n' + c.dim(' Esc de novo para parar o agente') + '\n');
1698
- }
1699
- return;
1700
- }
1701
- // Seta → no fim da linha aceita a sugestão inteira.
1702
- if (key && key.name === 'right' && fantasma && rl && rl.cursor === rl.line.length) {
1703
- const resto = fantasma;
1704
- fantasma = '';
1705
- rl.write(resto);
1706
- }
1707
- setImmediate(desenharFantasma);
1708
- });
1709
- }
1945
+ /* ── Esc Esc: parar o agente no meio do trabalho ──
1946
+ * Um Esc avisa; o segundo (em até 2s) manda parar. A caixa não sabe o
1947
+ * que é "parar o agente" — ela só avisa que o Esc aconteceu, e quem
1948
+ * decide o que fazer é aqui.
1949
+ *
1950
+ * A sugestão fantasma, que morava aqui, foi junto com o readline: hoje
1951
+ * ela é desenhada pela própria caixa (ver lib/entrada.js). */
1952
+ let ultimoEsc = 0;
1953
+ rl.on('escape', () => {
1954
+ if (!busy) { ultimoEsc = 0; return; }
1955
+ const agora = Date.now();
1956
+ if (agora - ultimoEsc < 2000) {
1957
+ ultimoEsc = 0;
1958
+ interromper();
1959
+ if (chatEmVoo) { try { chatEmVoo.abort(); } catch {} }
1960
+ console.log(c.warn(' ⏹ parando o agente…'));
1961
+ } else {
1962
+ ultimoEsc = agora;
1963
+ console.log(c.dim(' Esc de novo para parar o agente'));
1964
+ }
1965
+ });
1966
+
1967
+ // Ctrl+C com a caixa vazia: sair é sair, mas sem deixar o último turno
1968
+ // pela metade — o sayBye espera a gravação subir.
1969
+ rl.on('sigint', () => rl.close());
1710
1970
 
1711
1971
  function sayBye() {
1712
1972
  if (saidBye) return;
@@ -1729,6 +1989,10 @@ async function main() {
1729
1989
  async function drain() {
1730
1990
  if (busy) return;
1731
1991
  busy = true;
1992
+ // A caixa esmaece enquanto ele trabalha, e a segunda linha da barra
1993
+ // troca os atalhos pelo que importa naquele momento: como parar.
1994
+ state.trabalhando = true;
1995
+ if (rl.trabalhando) rl.trabalhando(true);
1732
1996
  while (queue.length) {
1733
1997
  const text = queue.shift().trim();
1734
1998
  if (!text) continue;
@@ -1740,6 +2004,8 @@ async function main() {
1740
2004
  }
1741
2005
  }
1742
2006
  busy = false;
2007
+ state.trabalhando = false;
2008
+ if (rl.trabalhando) rl.trabalhando(false);
1743
2009
  // Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
1744
2010
  // — senão ela aparece antes do resultado do último pedido.
1745
2011
  if (closed) sayBye();
@@ -1747,10 +2013,6 @@ async function main() {
1747
2013
  }
1748
2014
 
1749
2015
  rl.on('line', (line) => {
1750
- // A lista de setinhas (e o slider do /effort) são donos do teclado
1751
- // enquanto estão no ar: o Enter que confirma a escolha não é uma
1752
- // linha para a fila, e sem isto ele vira um pedido em branco.
1753
- if (sliderAtivo) return;
1754
2016
  if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
1755
2017
  // Abertura correndo: guarda em vez de mandar ao agente.
1756
2018
  if (segurando) { adiantadas.push(line); return; }
@@ -1786,31 +2048,7 @@ async function main() {
1786
2048
  interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
1787
2049
  conectado: () => conta.atual(),
1788
2050
  conectar: () => handleSlash('/connect').then(() => conta.atual()),
1789
- /* A ESCOLHA, COM AS SETINHAS — E O QUE FOI DIGITADO ANTES DELA.
1790
- *
1791
- * A lista só aparece depois de o plano voltar do servidor, e quem
1792
- * colou "3⏎1⏎/version" já respondeu antes de ver. Esse texto está em
1793
- * `adiantadas`; se a lista ignorasse, a resposta se perderia e o
1794
- * menu ficaria esperando um teclado que já falou. Então: primeiro o
1795
- * que foi adiantado (se casar com algum item), depois as setinhas. O
1796
- * que não casa fica na fila — é comando, não resposta. */
1797
- selecionar: ({ titulo, ajuda, lista }) => {
1798
- while (adiantadas.length) {
1799
- const dito = String(adiantadas[0] || '').trim();
1800
- if (!dito) { adiantadas.shift(); continue; }
1801
- const qual = boasVindas.escolher(dito, lista);
1802
- if (!qual) break; // isso é comando, não escolha
1803
- adiantadas.shift();
1804
- const item = lista.find((e) => e.id === qual);
1805
- console.log('');
1806
- console.log(c.bold(c.brand(' ' + titulo)));
1807
- console.log(c.brand(' ❯ ') + c.white(item ? item.titulo : qual));
1808
- return Promise.resolve(qual);
1809
- }
1810
- return menuSetas({ titulo, ajuda, lista });
1811
- },
1812
2051
  sessao: () => conta.sessao(),
1813
- aplicar: (cmd) => handleSlash(cmd),
1814
2052
  }).then((r) => {
1815
2053
  // O bloqueio de quem não é Premium/Super. É de CORTESIA: este arquivo
1816
2054
  // vai publicado no npm com o código à vista. Quem segura de verdade é