primocode 8.42.1 → 9.0.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 (102) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +599 -327
  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 +31 -192
  7. package/lib/catalogo.js +60 -60
  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 +545 -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 +606 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +13 -1
  23. package/lib/marca.js +4 -1
  24. package/lib/marketplace.js +189 -0
  25. package/lib/permissao.js +137 -0
  26. package/lib/registro.js +229 -0
  27. package/lib/skills.js +329 -0
  28. package/lib/tools.js +51 -181
  29. package/lib/ui.js +7 -3
  30. package/lib/voz.js +239 -0
  31. package/package.json +12 -3
  32. package/skills/acessibilidade/SKILL.md +28 -0
  33. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  34. package/skills/depurar-de-verdade/SKILL.md +29 -0
  35. package/skills/editar-meu-video/SKILL.md +59 -0
  36. package/skills/escrever-em-portugues/SKILL.md +28 -0
  37. package/skills/git-limpo/SKILL.md +28 -0
  38. package/skills/motion-de-primeira/SKILL.md +62 -0
  39. package/skills/pagina-que-converte/SKILL.md +28 -0
  40. package/skills/performance-web/SKILL.md +28 -0
  41. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  42. package/skills/slides-que-prendem/SKILL.md +40 -0
  43. package/studio/edge.py +16 -66
  44. package/studio/narrar.py +274 -0
  45. package/studio/pessoa.py +167 -0
  46. package/voz/primo_voz/__init__.py +0 -0
  47. package/voz/primo_voz/__main__.py +325 -0
  48. package/voz/primo_voz/escuta.py +235 -0
  49. package/voz/primo_voz/orbe.py +216 -0
  50. package/lib/audio-livre.js +0 -177
  51. package/lib/chaves.js +0 -149
  52. package/lib/claude-engine.js +0 -631
  53. package/lib/codex-engine.js +0 -208
  54. package/lib/remotion.js +0 -663
  55. package/lib/studio.js +0 -960
  56. package/studio/PRIMOCODE.md +0 -58
  57. package/studio/README.md +0 -154
  58. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  59. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  60. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  61. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  62. package/studio/ferramentas/00-protocolo.md +0 -125
  63. package/studio/ferramentas/01-modelos.md +0 -270
  64. package/studio/ferramentas/README.md +0 -32
  65. package/studio/ferramentas/corte.md +0 -919
  66. package/studio/ferramentas/grade.md +0 -92
  67. package/studio/ferramentas/prisma.md +0 -107
  68. package/studio/ferramentas/prosa.md +0 -86
  69. package/studio/ferramentas/tela.md +0 -93
  70. package/studio/ferramentas/traco.md +0 -79
  71. package/studio/server.py +0 -2254
  72. package/studio/studio.log +0 -57
  73. package/studio/voz.py +0 -388
  74. package/studio/web/analise.html +0 -52
  75. package/studio/web/core.css +0 -1167
  76. package/studio/web/fontes/bebas.woff2 +0 -0
  77. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  78. package/studio/web/fontes/inter-400.woff2 +0 -0
  79. package/studio/web/fontes/inter-700.woff2 +0 -0
  80. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  81. package/studio/web/fontes/playfair-700.woff2 +0 -0
  82. package/studio/web/fontes.css +0 -23
  83. package/studio/web/home.html +0 -217
  84. package/studio/web/js/analise.js +0 -510
  85. package/studio/web/js/audio.js +0 -607
  86. package/studio/web/js/editor.js +0 -1802
  87. package/studio/web/js/exportar.js +0 -222
  88. package/studio/web/js/ferramentas.js +0 -108
  89. package/studio/web/js/icones.js +0 -120
  90. package/studio/web/js/midia.js +0 -157
  91. package/studio/web/js/narracao.js +0 -325
  92. package/studio/web/js/player.js +0 -903
  93. package/studio/web/js/render-grade.js +0 -243
  94. package/studio/web/js/render-prosa.js +0 -104
  95. package/studio/web/js/render.js +0 -1852
  96. package/studio/web/js/sfx.js +0 -233
  97. package/studio/web/js/video.js +0 -464
  98. package/studio/web/js/voz-neural.js +0 -152
  99. package/studio/web/render.css +0 -1352
  100. package/studio/web/tf.html +0 -15
  101. package/studio/web/tool.html +0 -78
  102. 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,35 +252,98 @@ 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('❯')} `;
336
+ const primeira = partes.join(c.dim(' | '));
337
+
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];
344
343
  }
345
- function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
344
+
345
+ // A barra se redesenha sozinha a cada quadro; isto só pede um quadro novo.
346
+ function refreshPrompt() { if (rl) rl.prompt(); }
346
347
 
347
348
  // Enquanto uma pergunta está no ar, a próxima linha digitada é a RESPOSTA dela
348
349
  // — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
@@ -525,6 +526,195 @@ function prepararPasta(userText) {
525
526
  return plan;
526
527
  }
527
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
+
528
718
  async function sendPrompt(userText) {
529
719
  /* O portão do plano. Sem ele o bloqueio da abertura seria só um texto na
530
720
  tela: a pessoa leria "você não tem acesso" e mandaria a primeira coisa
@@ -587,6 +777,11 @@ async function sendPrompt(userText) {
587
777
 
588
778
  let r = null;
589
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();
590
785
  for (let i = 0; i < passos.length; i++) {
591
786
  if (i > 0) console.log(c.dim(` ⎿ ${passos[i].titulo}`));
592
787
  // Só fato de passo CONCLUÍDO entra aqui. Num pedido real o passo de
@@ -614,7 +809,10 @@ async function sendPrompt(userText) {
614
809
  // Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
615
810
  // não é motivo para criar pasta de projeto.
616
811
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
617
- 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,
618
816
  });
619
817
 
620
818
  r = await rodarPasso();
@@ -626,11 +824,18 @@ async function sendPrompt(userText) {
626
824
  console.log(c.dim(' ⎿ o passo não concluiu — tentando de novo'));
627
825
  r = await rodarPasso();
628
826
  }
827
+ registro.juntar(livro, r.registro);
629
828
  jaFeito = (r.concluiu !== false)
630
829
  ? String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200)
631
830
  : '';
632
831
  if (r.parado) break; // Esc Esc no meio do plano para o plano inteiro
633
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);
634
839
  state.history.push({ role: 'user', content: userText });
635
840
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
636
841
  state.lastResponse = r.assistantMessage.content;
@@ -675,30 +880,39 @@ async function sendPrompt(userText) {
675
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;
676
881
  const RE_CRIAR_DEPOIS = /\b(cri[ae]|faz|faça|mont[ae]|ger[ae]|escrev[ae]|produz|desenvolv)\w*/i;
677
882
 
678
- /** "baixa/exporta o vídeo" resolvido na hora, sem gastar modelo. */
679
- // "me manda o vídeo", "quero pra mandar no zap", "gera o arquivo" — a lista
680
- // era curta e o pedido sai de muitos jeitos. Fora dela, o pedido virava
681
- // tarefa de agente: gasto de API para abrir uma URL que o CLI já sabe.
682
- 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;
683
- 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;
684
896
 
685
897
  async function baixarCriacaoDireto(texto) {
686
898
  if (!(RE_BAIXAR.test(texto) && RE_PECA_STUDIO.test(texto))) return false;
687
899
  try {
688
- const studio = require('../lib/studio');
689
- const r = await studio.listar();
690
- const lista = (r && r.arquivos) || [];
691
- // a criação mais nova que é vídeo; sem vídeo, a mais nova de todas
692
- const alvo = lista.find((a) => a.ferramenta === 'corte') || lista[0];
693
- if (!alvo) return false;
694
- const url = `http://localhost:7777/v/${alvo.id}#gravar`;
695
- await tools.execute('open_in_browser', { target: url }, { projectDir: state.workDir || os.homedir() });
696
- console.log(c.ok(` ⎿ abrindo "${alvo.titulo}" para gravar`));
697
- console.log(c.muted(' O navegador vai pedir permissão de tela — aceite (marque "compartilhar áudio da aba"'));
698
- 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í.`));
699
913
  return true;
700
914
  } catch {
701
- return false; // Studio fora do ar ou sem criações: segue o caminho normal
915
+ return false; // sem projeto renderizado: segue o caminho normal
702
916
  }
703
917
  }
704
918
 
@@ -807,17 +1021,6 @@ async function runChat(promptForModel, userTextForMemory) {
807
1021
 
808
1022
  // ── Slash commands ─────────────────────────────────────────────────────
809
1023
 
810
- /* Qual motor executa este turno.
811
- *
812
- * Um `if` a mais aqui e um `if` a menos em qualquer outro lugar seria o
813
- * caminho para o /codex funcionar no modo agente e não no chat, ou ao
814
- * contrário. Um lugar só decide, e os dois modos chamam este. */
815
- function motorDaVez() {
816
- if (state.engine === 'claude-code') return claudeEngine;
817
- if (state.engine === 'codex') return codexEngine;
818
- return api;
819
- }
820
-
821
1024
  function isSlash(text) { return text.trim().startsWith('/'); }
822
1025
 
823
1026
  /* ── OS NOMES ANTIGOS ────────────────────────────────────────────────────
@@ -841,8 +1044,6 @@ const RENOMEADOS = {
841
1044
  entrar: 'connect', login: 'connect', conectar: 'connect',
842
1045
  conta: 'account',
843
1046
  sair: 'exit', // sair é SAIR; desconectar a conta é /logout
844
- // as chaves de API
845
- chave: 'key', chaves: 'key',
846
1047
  // o histórico
847
1048
  conversas: 'chats', historico: 'chats', histórico: 'chats',
848
1049
  // o resto
@@ -954,97 +1155,6 @@ async function handleSlash(text) {
954
1155
  break;
955
1156
  }
956
1157
 
957
- /* A chave de API da própria pessoa. Guardada em ~/.primocode/
958
- chaves.json com permissão 600 — nunca no config.json, que é o
959
- arquivo que se cola num chamado de suporte. */
960
- case 'key': case 'keys': {
961
- const [qual, ...resto] = rest.split(/\s+/).filter(Boolean);
962
- const valor = resto.join(' ');
963
-
964
- if (!qual) {
965
- console.log(c.white('\n Suas chaves de API'));
966
- for (const p of chaves.listar()) {
967
- console.log(' ' + c.white(p.nome.padEnd(8))
968
- + (p.tem ? c.ok(p.disfarce) : c.dim('não configurada'))
969
- + c.dim(` ${p.variavel}`));
970
- }
971
- console.log(c.dim('\n /key claude sk-ant-… guarda a chave'));
972
- console.log(c.dim(' /key gpt remover apaga a que estiver lá'));
973
- console.log(c.muted('\n Sem chave própria, cada motor usa a conta em que você já entrou nele.'));
974
- break;
975
- }
976
- if (!chaves.PROVEDORES[qual]) {
977
- console.log(c.err(` Não conheço "${qual}". Use: ${Object.keys(chaves.PROVEDORES).join(', ')}`));
978
- break;
979
- }
980
- if (valor === 'remover' || valor === 'apagar') {
981
- const r = chaves.esquecer(qual);
982
- console.log(r.ok ? c.ok(` Chave do ${chaves.PROVEDORES[qual].nome} apagada.`)
983
- : c.muted(` ${r.error}`));
984
- break;
985
- }
986
- if (!valor) {
987
- const p = chaves.PROVEDORES[qual];
988
- console.log(c.muted(` Uso: `) + c.brand(`/key ${qual} ${p.prefixo}…`));
989
- console.log(c.dim(` Pegue a sua em ${p.onde}`));
990
- break;
991
- }
992
- const r = chaves.guardar(qual, valor);
993
- if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
994
- // O que se mostra de volta é o disfarce, nunca a chave: a linha
995
- // fica no scrollback do terminal e no print que a pessoa manda.
996
- console.log(c.ok(` Chave do ${r.nome} guardada.`) + c.muted(` ${r.disfarce}`));
997
- console.log(c.dim(` Ela vai como ${chaves.PROVEDORES[qual].variavel} só quando o motor rodar.`));
998
- break;
999
- }
1000
-
1001
- /* O Codex da conta da pessoa, com o modelo barato.
1002
- O protocolo é o MESMO do /claude — ver lib/codex-engine.js. */
1003
- /* `/gpt` e `/codex` são a mesma coisa e existem os dois de propósito:
1004
- o programa se chama Codex, mas quem usa fala "GPT" — e o comando que
1005
- a pessoa tenta é o nome que ela tem na cabeça, não o do fabricante. */
1006
- case 'gpt': case 'codex': {
1007
- if (rest.toLowerCase() === 'off') {
1008
- if (state.engine !== 'codex') { console.log(c.muted(' Já estava no motor padrão.')); break; }
1009
- state.engine = 'server';
1010
- console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
1011
- break;
1012
- }
1013
- console.log(c.dim(' Checando o Codex…'));
1014
- const r = codexEngine.garantirEngine();
1015
- if (!r.ok) {
1016
- console.log(c.err(` ${r.error}`));
1017
- if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
1018
- break;
1019
- }
1020
- state.engine = 'codex';
1021
- console.log(c.ok(` Motor: Codex (${r.modelo})`) + c.muted(' — na sua conta, não na minha'));
1022
- console.log(c.muted(' /codex off volta pro motor padrão.'));
1023
- break;
1024
- }
1025
-
1026
- case 'claude': {
1027
- if (rest.toLowerCase() === 'off') {
1028
- if (state.engine !== 'claude-code') { 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 Claude Code e ORCA…'));
1034
- const r = await claudeEngine.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 = 'claude-code';
1041
- const onde = r.caminho === 'A' ? 'sessão contínua via ORCA' : 'chamada avulsa (claude -p, sem ORCA)';
1042
- console.log(c.ok(` Motor: Claude Code (${onde})`)
1043
- + c.muted(` — ${claudeEngine.MODELO_CLAUDE || 'o padrão da sua conta'}, na sua conta`));
1044
- console.log(c.muted(' /claude off volta pro motor padrão.'));
1045
- break;
1046
- }
1047
-
1048
1158
  // O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
1049
1159
  // Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
1050
1160
  // ("/effort high") aplica direto, para quem já sabe o que quer.
@@ -1244,6 +1354,191 @@ async function handleSlash(text) {
1244
1354
  break;
1245
1355
  }
1246
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
+
1247
1542
  /* A janela. "Se mandar /rc pra ele, ele abre o aplicativo — para o
1248
1543
  Mac e o Windows, já instalado junto com o npm, com as cores do
1249
1544
  PrimoCode. Um chat estilo terminal para quem não quer o terminal."
@@ -1332,29 +1627,31 @@ async function handleSlash(text) {
1332
1627
  abrir: (u) => tools.openInBrowser(u),
1333
1628
  contar: (info) => {
1334
1629
  sp.stop();
1335
- /* O CÓDIGO NÃO APARECE MAIS NO TERMINAL.
1336
- *
1337
- * "Pode tirar essa parte no login onde aparece o código
1338
- * — o terminal mandou o código, pode tirar."
1339
- *
1340
- * A página do token já resolve o pareamento sozinha: ela
1341
- * pergunta o código ao 127.0.0.1 desta máquina, e onde o
1342
- * navegador bloqueia isso (Safari) ela oferece "Colar" —
1343
- * o código continua indo para a área de transferência,
1344
- * que é o caminho sem leitura. Oito letras impressas aqui
1345
- * eram uma tarefa para a pessoa que o produto já não
1346
- * precisa, e a tela ficava dizendo que talvez precisasse.
1347
- *
1348
- * O código continua existindo no protocolo (lib/conta.js)
1349
- * — 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;
1350
1632
  console.log(c.muted(' Abrindo no navegador:'));
1351
1633
  console.log(' ' + c.brand(info.url.replace(/[?&]porta=\d+/, '')));
1352
1634
  console.log('');
1353
- console.log(' ' + c.white('Entre com a sua conta na página que abriu — o resto é automático.'));
1354
- if (!info.porta && info.copiado) {
1355
- // Sem a porta local, quem fecha o pareamento é a área de
1356
- // transferência: a página pede um toque em "Colar".
1357
- 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.')));
1358
1655
  }
1359
1656
  console.log(c.dim(' Esperando o navegador…'));
1360
1657
  },
@@ -1467,6 +1764,25 @@ async function handleSlash(text) {
1467
1764
  // rl.close() em vez de process.exit(): sair na marra descarta o que
1468
1765
  // ainda estava no buffer de saída.
1469
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
+
1470
1786
  default: {
1471
1787
  // "/" sozinho abre o menu; um prefixo único autopreenche e executa
1472
1788
  // ("/eff" → /effort); prefixo ambíguo mostra as opções.
@@ -1611,7 +1927,11 @@ async function main() {
1611
1927
  userName: state.config.userName,
1612
1928
  pastaPadrao: state.workDirOrigem === 'padrao',
1613
1929
  }));
1614
- 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
+ });
1615
1935
 
1616
1936
  rl.prompt();
1617
1937
 
@@ -1622,57 +1942,31 @@ async function main() {
1622
1942
  let closed = false;
1623
1943
  let saidBye = false;
1624
1944
 
1625
- // ── Sugestão fantasma: o autopreenchimento VISÍVEL enquanto digita ──
1626
- // "/str" mostra "eaming" esmaecido na frente do cursor, como o Claude
1627
- // Code. Seta → (ou Tab) aceita; Enter com o pedaço já executa. Desenhada
1628
- // DEPOIS de o readline processar a tecla (setImmediate), por cima do que
1629
- // sobrou da linha (\\x1b[0K limpa), e o cursor volta para o lugar.
1630
- if (process.stdin.isTTY && process.stdout.isTTY) {
1631
- let fantasma = '';
1632
- const desenharFantasma = () => {
1633
- if (busy || pendingAsk || sliderAtivo || !rl) return;
1634
- const linha = rl.line;
1635
- const noFim = rl.cursor === linha.length;
1636
- fantasma = '';
1637
- if (noFim && linha.startsWith('/') && !/\s/.test(linha) && linha.length > 1) {
1638
- const baixa = linha.toLowerCase();
1639
- const hit = SLASH_COMANDOS.map(([n]) => n).find((n) => n.startsWith(baixa) && n.length > linha.length);
1640
- if (hit) fantasma = hit.slice(linha.length);
1641
- }
1642
- if (!noFim) return; // cursor no meio: não mexe no que vem depois dele
1643
- // Limpa resto da linha (fantasma antigo) e desenha o novo, voltando o cursor.
1644
- process.stdout.write('\x1b[0K' + (fantasma ? `\x1b[2m${fantasma}\x1b[0m\x1b[${fantasma.length}D` : ''));
1645
- };
1646
- // ── Esc Esc: parar o agente no meio do trabalho ──
1647
- // Um Esc avisa; o segundo (em até 2s) manda parar. Dois Escs tão
1648
- // rápidos que chegam colados num chunk só viram UMA tecla com
1649
- // sequence "\x1b\x1b" — vale como os dois.
1650
- let ultimoEsc = 0;
1651
- process.stdin.on('keypress', (ch, key) => {
1652
- if (sliderAtivo) return; // o slider é dono do teclado enquanto está no ar
1653
- if (key && (key.name === 'escape' || key.sequence === '\x1b\x1b')) {
1654
- if (!busy) { ultimoEsc = 0; return; } // sem agente rodando, Esc é só Esc
1655
- const agora = Date.now();
1656
- if (key.sequence === '\x1b\x1b' || agora - ultimoEsc < 2000) {
1657
- ultimoEsc = 0;
1658
- interromper();
1659
- if (chatEmVoo) { try { chatEmVoo.abort(); } catch {} }
1660
- process.stdout.write('\n' + c.warn(' ⏹ parando o agente…') + '\n');
1661
- } else {
1662
- ultimoEsc = agora;
1663
- process.stdout.write('\n' + c.dim(' Esc de novo para parar o agente') + '\n');
1664
- }
1665
- return;
1666
- }
1667
- // Seta → no fim da linha aceita a sugestão inteira.
1668
- if (key && key.name === 'right' && fantasma && rl && rl.cursor === rl.line.length) {
1669
- const resto = fantasma;
1670
- fantasma = '';
1671
- rl.write(resto);
1672
- }
1673
- setImmediate(desenharFantasma);
1674
- });
1675
- }
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());
1676
1970
 
1677
1971
  function sayBye() {
1678
1972
  if (saidBye) return;
@@ -1695,6 +1989,10 @@ async function main() {
1695
1989
  async function drain() {
1696
1990
  if (busy) return;
1697
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);
1698
1996
  while (queue.length) {
1699
1997
  const text = queue.shift().trim();
1700
1998
  if (!text) continue;
@@ -1706,6 +2004,8 @@ async function main() {
1706
2004
  }
1707
2005
  }
1708
2006
  busy = false;
2007
+ state.trabalhando = false;
2008
+ if (rl.trabalhando) rl.trabalhando(false);
1709
2009
  // Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
1710
2010
  // — senão ela aparece antes do resultado do último pedido.
1711
2011
  if (closed) sayBye();
@@ -1713,10 +2013,6 @@ async function main() {
1713
2013
  }
1714
2014
 
1715
2015
  rl.on('line', (line) => {
1716
- // A lista de setinhas (e o slider do /effort) são donos do teclado
1717
- // enquanto estão no ar: o Enter que confirma a escolha não é uma
1718
- // linha para a fila, e sem isto ele vira um pedido em branco.
1719
- if (sliderAtivo) return;
1720
2016
  if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
1721
2017
  // Abertura correndo: guarda em vez de mandar ao agente.
1722
2018
  if (segurando) { adiantadas.push(line); return; }
@@ -1752,31 +2048,7 @@ async function main() {
1752
2048
  interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
1753
2049
  conectado: () => conta.atual(),
1754
2050
  conectar: () => handleSlash('/connect').then(() => conta.atual()),
1755
- /* A ESCOLHA, COM AS SETINHAS — E O QUE FOI DIGITADO ANTES DELA.
1756
- *
1757
- * A lista só aparece depois de o plano voltar do servidor, e quem
1758
- * colou "3⏎1⏎/version" já respondeu antes de ver. Esse texto está em
1759
- * `adiantadas`; se a lista ignorasse, a resposta se perderia e o
1760
- * menu ficaria esperando um teclado que já falou. Então: primeiro o
1761
- * que foi adiantado (se casar com algum item), depois as setinhas. O
1762
- * que não casa fica na fila — é comando, não resposta. */
1763
- selecionar: ({ titulo, ajuda, lista }) => {
1764
- while (adiantadas.length) {
1765
- const dito = String(adiantadas[0] || '').trim();
1766
- if (!dito) { adiantadas.shift(); continue; }
1767
- const qual = boasVindas.escolher(dito, lista);
1768
- if (!qual) break; // isso é comando, não escolha
1769
- adiantadas.shift();
1770
- const item = lista.find((e) => e.id === qual);
1771
- console.log('');
1772
- console.log(c.bold(c.brand(' ' + titulo)));
1773
- console.log(c.brand(' ❯ ') + c.white(item ? item.titulo : qual));
1774
- return Promise.resolve(qual);
1775
- }
1776
- return menuSetas({ titulo, ajuda, lista });
1777
- },
1778
2051
  sessao: () => conta.sessao(),
1779
- aplicar: (cmd) => handleSlash(cmd),
1780
2052
  }).then((r) => {
1781
2053
  // O bloqueio de quem não é Premium/Super. É de CORTESIA: este arquivo
1782
2054
  // vai publicado no npm com o código à vista. Quem segura de verdade é