primocode 9.10.0 → 10.0.9

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 (89) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +169 -342
  3. package/lib/act.js +360 -39
  4. package/lib/browser.js +105 -4
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/claude-engine.js +38 -17
  8. package/lib/codex-engine.js +27 -9
  9. package/lib/config.js +10 -3
  10. package/lib/cursor.js +10 -1
  11. package/lib/entrada.js +77 -11
  12. package/lib/estudio/cinema.js +27 -9
  13. package/lib/estudio/diretor.js +21 -1
  14. package/lib/estudio/ende.js +16 -5
  15. package/lib/estudio/estilo.js +66 -0
  16. package/lib/estudio/gerativo.js +1453 -0
  17. package/lib/estudio/glifos.js +215 -0
  18. package/lib/estudio/hibrido.js +165 -0
  19. package/lib/estudio/index.js +284 -53
  20. package/lib/estudio/kit/base.jsx +285 -0
  21. package/lib/estudio/kit/gerativo.jsx +980 -0
  22. package/lib/estudio/kit/index.jsx +13 -0
  23. package/lib/estudio/kit/marca.jsx +741 -0
  24. package/lib/estudio/kit/tres.jsx +308 -0
  25. package/lib/estudio/kit/vox.jsx +357 -0
  26. package/lib/estudio/kit.js +78 -0
  27. package/lib/estudio/lancamento.js +441 -0
  28. package/lib/estudio/motion.js +2 -5
  29. package/lib/estudio/plano.js +28 -3
  30. package/lib/estudio/referencias.js +105 -0
  31. package/lib/estudio/remotion.js +139 -51
  32. package/lib/estudio/ritmo.js +77 -0
  33. package/lib/estudio/sons-leves.js +38 -1
  34. package/lib/estudio/variedade.js +105 -0
  35. package/lib/estudio/vox.js +414 -0
  36. package/lib/fala.js +1 -94
  37. package/lib/mcp.js +156 -0
  38. package/lib/personas.js +67 -0
  39. package/lib/pipeline.js +55 -56
  40. package/lib/repeticao.js +3 -1
  41. package/lib/skills.js +44 -2
  42. package/lib/snapshot.js +111 -0
  43. package/lib/tools.js +100 -22
  44. package/lib/ui.js +50 -8
  45. package/lib/video/analise.js +89 -0
  46. package/lib/video/auto.js +329 -0
  47. package/lib/video/camadas.js +207 -0
  48. package/lib/video/cli.js +74 -0
  49. package/lib/video/delta.js +90 -0
  50. package/lib/video/edits.js +379 -0
  51. package/lib/video/filters.js +280 -0
  52. package/lib/video/fx3d/Camadas.jsx +64 -0
  53. package/lib/video/fx3d/Objetos.jsx +128 -0
  54. package/lib/video/fx3d/Root.jsx +18 -0
  55. package/lib/video/fx3d/modelos.jsx +304 -0
  56. package/lib/video/fx3d.js +108 -0
  57. package/lib/video/gerados.js +119 -0
  58. package/lib/video/objetos.js +111 -0
  59. package/lib/video/ops-fx.js +453 -0
  60. package/lib/video/ops.js +347 -0
  61. package/lib/video/paralelo.js +50 -0
  62. package/lib/video/pedido.js +191 -0
  63. package/lib/video/projeto.js +133 -0
  64. package/lib/video/rastrear.js +95 -0
  65. package/lib/video/rastro.js +276 -0
  66. package/lib/video/render.js +274 -0
  67. package/lib/video/texto.js +144 -0
  68. package/lib/video/timeline.js +318 -0
  69. package/lib/video/transcript.js +124 -0
  70. package/lib/voz.js +75 -223
  71. package/package.json +2 -3
  72. package/skills/motion-de-primeira/SKILL.md +5 -5
  73. package/studio/narrar.py +10 -7
  74. package/studio/rastrear.py +399 -0
  75. package/studio/texto.py +208 -0
  76. package/voz/primo_voz/__main__.py +96 -236
  77. package/voz/primo_voz/transcrever.py +31 -1
  78. package/app/ansi.js +0 -55
  79. package/app/app.css +0 -500
  80. package/app/app.js +0 -1017
  81. package/app/icone.svg +0 -6
  82. package/app/index.html +0 -220
  83. package/app/primo-marca.js +0 -192
  84. package/lib/app.js +0 -615
  85. package/lib/icone.js +0 -263
  86. package/lib/instalar-app.js +0 -231
  87. package/lib/painel.js +0 -291
  88. package/voz/primo_voz/desperta.py +0 -195
  89. package/voz/primo_voz/orbe.py +0 -406
package/bin/primocode.js CHANGED
@@ -22,7 +22,7 @@ const tools = require('../lib/tools');
22
22
  const cfg = require('../lib/config');
23
23
  const nomeProjeto = require('../lib/nome-projeto');
24
24
  const provedorLocal = require('../lib/local/provedor');
25
- const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
25
+ const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro, mandarDuranteATarefa, sobrasDaCaixa } = require('../lib/act');
26
26
  const fala = require('../lib/fala');
27
27
  const boasVindas = require('../lib/boas-vindas');
28
28
  const pasta = require('../lib/pasta');
@@ -37,7 +37,6 @@ const checkpoint = require('../lib/checkpoint');
37
37
  const comandosLib = require('../lib/comandos');
38
38
  const marketplace = require('../lib/marketplace');
39
39
  const vozModo = require('../lib/voz');
40
- const appLib = require('../lib/app');
41
40
  const conta = require('../lib/conta');
42
41
  const nuvem = require('../lib/nuvem');
43
42
  const compat = require('../lib/compatibilidade');
@@ -63,6 +62,17 @@ const tempos = require('../lib/tempos');
63
62
  process.on('uncaughtException', (e) => {
64
63
  fala.problema(e);
65
64
  });
65
+ /* O Node sai com 0 quando o laço de eventos esvazia — e uma promessa que
66
+ nunca resolve, sem timer nem socket vivo, faz exatamente isso: o pedido
67
+ "termina" calado no meio (visto no vídeo de escopo fechado, 01/10). Com
68
+ tarefa em andamento, sair assim é bug: diz, e sai com erro. */
69
+ process.on('beforeExit', () => {
70
+ if (global.__primocodeTarefa) {
71
+ process.stderr.write(`\n ⚠ o PrimoCode ia sair no meio da tarefa (${global.__primocodeTarefa}). Diga "continua" para retomar.\n`);
72
+ process.exitCode = 3;
73
+ global.__primocodeTarefa = null;
74
+ }
75
+ });
66
76
  process.on('unhandledRejection', (e) => {
67
77
  fala.problema(e instanceof Error ? e : new Error(String(e)));
68
78
  });
@@ -104,59 +114,6 @@ const state = {
104
114
  workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
105
115
  };
106
116
 
107
- // A janela do /rc, quando aberta. Uma por sessão: o segundo /rc traz a que
108
- // existe para a frente em vez de subir outro servidor ao lado.
109
- let janelaAberta = null;
110
-
111
- /* O que o /rc, o /ide e o `primo --app` passam ao lib/app.js. Uma função
112
- só porque os três sobem a MESMA janela: se cada um montasse a sua, o /ide
113
- aberto depois do /rc acharia um servidor sem a pasta do projeto — e o
114
- painel da prévia nasceria vazio. */
115
- function opcoesDaJanela(extra) {
116
- return Object.assign({
117
- versao: VERSION,
118
- enviar: (t) => sendPrompt(t),
119
- barra: (t) => handleSlash(t),
120
- interromper,
121
- estado: () => ({
122
- pasta: state.workDir ? shortPath(state.workDir) : null,
123
- projeto: state.currentProject,
124
- modo: state.mode,
125
- esforco: state.config.effort,
126
- permissoes: perigo.normalizarModo(state.config.permissoes),
127
- voz: !!vozEmEspera,
128
- usuario: state.config.userName || null,
129
- mascote: state.config.mascot || 'primo',
130
- anexos: state.attachedFiles.map((f) => f.path),
131
- }),
132
- // Lida na hora, não guardada: o /dir e o /project trocam a pasta
133
- // com a janela aberta, e o painel tem de acompanhar.
134
- pastaProjeto: () => projectDir(),
135
- /* As ações da janela passam pelos MESMOS caminhos dos comandos — o
136
- /projetos, o /clear, o /permissoes, o /effort, o /attach — para a
137
- janela e o terminal nunca discordarem sobre onde se está. */
138
- entrarProjeto: (dir) => {
139
- entrarNoProjeto(dir, 'projeto');
140
- console.log(c.ok(` Trabalhando em ${state.currentProject}`) + c.muted(` (${shortPath(dir)})`));
141
- refreshPrompt();
142
- },
143
- novoChat: () => handleSlash('/clear'),
144
- mudarPermissoes: (m) => handleSlash('/permissoes ' + m),
145
- mudarEsforco: (n) => aplicarEffort(n),
146
- anexar: (f) => {
147
- state.attachedFiles.push(f);
148
- console.log(c.ok(`Anexado: ${f.path} (${tools.fmtBytes(f.content.length)})`) + c.muted(' pela janela'));
149
- refreshPrompt();
150
- },
151
- desanexar: (nome) => {
152
- state.attachedFiles = nome ? state.attachedFiles.filter((f) => f.path !== nome) : [];
153
- refreshPrompt();
154
- },
155
- voz: (ligar) => handleSlash(ligar ? '/voice' : '/voice parar'),
156
- continuarConversa: (alvo) => abrirConversa(alvo),
157
- }, extra || {});
158
- }
159
-
160
117
  const HELP = `
161
118
  ${c.bold('Comandos')}
162
119
  ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
@@ -179,8 +136,6 @@ ${c.bold('Comandos')}
179
136
  ${c.brand('/abrir')} abre o projeto: o servidor de pé, ou a index.html
180
137
  ${c.brand('/copiar')} copia a última resposta
181
138
  ${c.brand('/historico')} [termo] procura nos seus pedidos antigos
182
- ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
183
- ${c.brand('/ide')} abre a janela com a prévia do site e os arquivos ao lado
184
139
  ${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
185
140
  ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
186
141
  ${c.brand('/logout')} desconecta este computador da conta
@@ -237,14 +192,12 @@ const SLASH_COMANDOS = [
237
192
  ['/abrir', 'abre o projeto atual: o servidor de pé, ou a index.html'],
238
193
  ['/copiar', 'copia a última resposta'],
239
194
  ['/historico', 'procura nos seus pedidos antigos: /historico login'],
240
- ['/rc', 'abre a janela do PrimoCode'],
241
- ['/ide', 'abre a janela com a prévia do site e os arquivos ao lado'],
242
195
  ['/login', 'entra na sua conta Conecta Primo'],
243
196
  ['/account', 'sua conta, plano e computadores conectados'],
244
197
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
245
198
  ['/logout', 'desconecta este computador da conta'],
246
199
  ['!comando', 'roda direto no shell, sem passar pelo agente (o campo fica âmbar)'],
247
- ['/voice', 'fica ouvindo em segundo plano — diga "ei, primo" e a orb aparece'],
200
+ ['/voice', 'liga o microfone: você fala, ele faz, e a resposta sai em voz alta'],
248
201
  ['/voice parar', 'desliga o modo voz e fecha o microfone'],
249
202
  ['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
250
203
  ['/marketplace', 'a tela das skills: navegue, ligue, crie'],
@@ -669,7 +622,12 @@ function deriveProjectName(text) {
669
622
  * ("isso", "esse", "nele"). Todo o resto ("continua", "muda a cor", "agora
670
623
  * põe fases") fica. */
671
624
  const RE_PROJETO_NOVO = /\b(novo projeto|projeto novo|outro projeto|do zero|come[cç]a(r)? (um|uma|outro|outra))\b/;
672
- const RE_CRIAR_UM = /\b(crie|cria|criar|fa[cç]a|faz|fazer|monte|monta|montar|gere|gera|desenvolva|construa|programe|quero|preciso de)\s+(um|uma|outro|outra|novo|nova)\b/;
625
+ /* "crie UM site…" e também "crie O SITE da barbearia": o artigo definido com
626
+ o tipo da coisa é projeto novo com nome (medido em 29/09: "crie o site
627
+ bench-barbearia" nasceu DENTRO do projeto que estava aberto). Se for o
628
+ mesmo assunto do projeto atual, a comparação de assunto em pedeProjetoNovo
629
+ segura como continuação. */
630
+ const RE_CRIAR_UM = /\b(crie|cria|criar|fa[cç]a|faz|fazer|monte|monta|montar|gere|gera|desenvolva|construa|programe|quero|preciso de)\s+(um|uma|outro|outra|novo|nova|(o|a)\s+(site|app|aplicativo|jogo|projeto|landing|pagina|sistema|loja|dashboard|painel))\b/;
673
631
  const RE_REFERE_ATUAL = /\b(isso|isto|esse|essa|este|esta|nele|nela|dele|dela|no projeto|nesse|nessa|neste|nesta|continua|continue|edita|edite|muda|mude|melhora|melhore|adiciona|adicione|coloca|coloque|p[oõ]e|corrige|corrija|arruma|arrume|ajusta|ajuste)\b/;
674
632
 
675
633
  /* "crie uma nova seção", "cria um botão", "faz uma tela de login": é um
@@ -692,6 +650,12 @@ function pedeProjetoNovo(texto) {
692
650
  const RE_COMPUTADOR = /\b(abr[ae]|abrir|clica|clique|clicar|digita|digite|digitar|mouse|cursor|teclado|meu computador|no (meu )?mac|na (minha )?tela|aplicativo|spotify|safari|finder|whatsapp|mensagens|m[úu]sica|configura[çc][õo]es|ajustes)\b/i;
693
651
  function querComputador(texto) { return RE_COMPUTADOR.test(String(texto || '')); }
694
652
 
653
+ /* Pedido de VER o navegador ("abre o youtube", "entra no site x", "testa no
654
+ Chrome"). Sem isto as ferramentas browser_* rodam escondidas: pesquisar e
655
+ conferir não precisam abrir aba na tela da pessoa (02/10). */
656
+ const RE_NAVEGADOR = /\b(navegador|chrome|browser|edge|brave|aba|abas|youtube|gmail|instagram|facebook|linkedin|login|loga|logar|(abr[ae]|abrir|entr[ae]|entrar|acess[ae]|acessar|mostr[ae]|testa|testar)\b[^.?!]{0,25}\b(site|p[áa]gina|link|url|google|navegador))\b/i;
657
+ function querNavegador(texto) { return RE_NAVEGADOR.test(String(texto || '')); }
658
+
695
659
  /* Guarda onde se está, para a próxima sessão continuar aqui. */
696
660
  function entrarNoProjeto(dir, origem = 'projeto') {
697
661
  state.workDir = dir;
@@ -871,8 +835,15 @@ async function autorizarControle(oQueVaiFazer, { primeira, tipo } = {}) {
871
835
  + c.dim('2') + c.muted(' esta sessão inteira ')
872
836
  + c.dim('Enter') + c.muted(' não'));
873
837
 
874
- const dito = String(await ask(' ' + c.brand('❯ ') + c.muted('1, 2 ou Enter '))).trim().toLowerCase();
875
- if (dito === '1' || /^(s|sim|ok|pode|agora|y|yes)$/.test(dito)) {
838
+ // Sem pontuação: a resposta pode vir FALADA (/voice), e o Whisper devolve
839
+ // "Sim." e "Pode, sim." — que também são sim.
840
+ const dito = String(await ask(' ' + c.brand('❯ ') + c.muted('1, 2 ou Enter '))).trim().toLowerCase()
841
+ .replace(/[.!?,;:…]+/g, ' ').replace(/\s+/g, ' ').trim();
842
+ if (/sess|sempre|tudo|todas/.test(dito) && dito !== '1') {
843
+ console.log(' ' + c.ok('✔ autorizado') + c.dim(' — esta sessão inteira. Fechar o PrimoCode esquece.'));
844
+ return 'sessao';
845
+ }
846
+ if (dito === '1' || /^(s|sim|ok|pode|agora|y|yes|claro|vai)\b/.test(dito)) {
876
847
  console.log(' ' + c.ok('✔ autorizado') + c.dim(' — só esta ação'));
877
848
  return 'agora';
878
849
  }
@@ -884,28 +855,6 @@ async function autorizarControle(oQueVaiFazer, { primeira, tipo } = {}) {
884
855
  return 'nao';
885
856
  }
886
857
 
887
- /* ── A PERMISSÃO NO MODO VOZ ─────────────────────────────────────────────
888
- * No modo voz ninguém está olhando o terminal: a pergunta "1, 2 ou Enter"
889
- * ficava lá embaixo, o agente parava esperando, e para quem só falava ele
890
- * simplesmente travava. Abrir o /voice e PEDIR em voz para ele mexer no
891
- * computador já é a autorização — é o produto funcionando como o Jarvis. Fica
892
- * dito no terminal, a cada ação, o que ele fez; e fechar o PrimoCode esquece. */
893
- async function autorizarPorVoz(oQueVaiFazer, { primeira, tipo } = {}) {
894
- /* Pedir em voz é autorizar o que foi pedido — mas não o que não tem volta.
895
- Ninguém está lendo o terminal para ver um `rm -rf` passar: o perigoso
896
- é recusado, e o agente conta em voz o que ficou por fazer. */
897
- if (tipo === 'perigo') {
898
- console.log(' ' + c.warn('▸▸ recusado no modo voz: ') + c.muted(oQueVaiFazer));
899
- return 'nao';
900
- }
901
- if (tipo) return 'agora';
902
- if (primeira) {
903
- console.log(' ' + c.warn('▸▸') + ' ' + c.muted('Modo voz: o Primo vai usar o mouse e o teclado. Você vê o cursor dele na tela.'));
904
- }
905
- console.log(' ' + c.brand('●') + ' ' + c.dim(oQueVaiFazer));
906
- return 'sessao';
907
- }
908
-
909
858
  /* ── A TELA DO MARKETPLACE ───────────────────────────────────────────────
910
859
  * O desenho e as teclas vivem em lib/marketplace.js, que é função pura. O que
911
860
  * mora aqui é o que precisa do terminal: tirar a caixa de digitar da frente,
@@ -1119,6 +1068,42 @@ async function enviarPedido(userText) {
1119
1068
 
1120
1069
  const plan = prepararPasta(userText);
1121
1070
 
1071
+ // O Chrome só aparece na tela quando o pedido é de navegador (lib/browser.js, definirVisivel).
1072
+ require('../lib/browser.js').definirVisivel(querNavegador(userText));
1073
+
1074
+ /* AS REFERÊNCIAS DO VÍDEO, decididas pela pessoa antes de começar
1075
+ (lib/estudio/referencias.js): web aberta, só o escopo que ela der,
1076
+ ou nenhuma — só motion. No pedido único não há quem responda, e
1077
+ vale o aberto. */
1078
+ const referencias = require('../lib/estudio/referencias.js');
1079
+ referencias.limpar();
1080
+ const producao = require('../lib/estudio/diretor.js').precisaDeProducao(userText);
1081
+ if (producao.producao && producao.formato === 'video') {
1082
+ /* O modo sai do próprio pedido quando ele diz ("só motion", "só do
1083
+ site x.com", "web aberta"). Quando NÃO diz, pergunta antes de
1084
+ começar — "vídeo" às vezes quer dizer só o motion (02/10). No
1085
+ pedido único não há quem responda, e vale o aberto. */
1086
+ const r = referencias.inferir(userText);
1087
+ if (!r.explicito && process.stdin.isTTY && !state.semTerminal) {
1088
+ console.log('');
1089
+ console.log(' ' + c.brand('▸') + ' ' + c.bold(c.white('Que vídeo você quer?')));
1090
+ console.log(' ' + c.dim('1') + c.muted(' só motion — animação pura, nenhuma foto nem vídeo de fora'));
1091
+ console.log(' ' + c.dim('2') + c.muted(' motion + web aberta — ele pesquisa e busca imagem e vídeo à vontade'));
1092
+ console.log(' ' + c.dim('3') + c.muted(' motion + escopo fechado — só os sites e termos que você disser'));
1093
+ const dito = String(await ask(' ' + c.brand('❯ ') + c.muted('1, 2 ou 3 '))).trim();
1094
+ // Aqui 1 é o motion (a ordem da pergunta), não o 1 = aberto do entender().
1095
+ const modo = { 1: 'sem', 2: 'aberto', 3: 'fechado' }[(/^\s*([123])\b/.exec(dito) || [])[1]] || referencias.entender(dito) || 'aberto';
1096
+ r.modo = modo;
1097
+ r.escopo = modo === 'fechado'
1098
+ ? String(await ask(' ' + c.brand('❯ ') + c.muted('quais sites ou termos? (separados por vírgula) '))).trim()
1099
+ : '';
1100
+ if (modo === 'fechado' && !r.escopo) r.modo = 'aberto';
1101
+ }
1102
+ referencias.definir(r.modo, r.escopo);
1103
+ console.log(c.dim(` ⎿ referências: ${{ aberto: 'web aberta', fechado: 'só ' + r.escopo, sem: 'nenhuma, só motion' }[r.modo]}`)
1104
+ + (r.modo === 'aberto' ? c.dim(' · diga "só motion" ou "só do site x.com" para mudar') : ''));
1105
+ }
1106
+
1122
1107
  // O resumo da rodada anterior é feito em segundo plano logo depois
1123
1108
  // dela; aqui só se espera ele terminar, para não sair com memória velha.
1124
1109
  //
@@ -1185,7 +1170,8 @@ async function enviarPedido(userText) {
1185
1170
  permissoes: state.config.permissoes,
1186
1171
  // Pedido de mexer no computador leva a dica que libera as
1187
1172
  // ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
1188
- instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
1173
+ instrucao: [i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
1174
+ i === 0 ? require('../lib/estudio/referencias.js').instrucao() : ''].filter(Boolean).join('\n\n'),
1189
1175
  });
1190
1176
 
1191
1177
  r = await rodarPasso();
@@ -1418,7 +1404,7 @@ function resumirDepois() {
1418
1404
  atual.then(() => { if (state.resumindo === atual) state.resumindo = null; });
1419
1405
  }
1420
1406
 
1421
- async function esperarResumo(tetoMs = 15000) {
1407
+ async function esperarResumo(tetoMs = 6000) {
1422
1408
  if (!state.resumindo) return;
1423
1409
  await Promise.race([state.resumindo, new Promise((ok) => setTimeout(ok, tetoMs))]);
1424
1410
  }
@@ -1473,7 +1459,10 @@ let chatEmVoo = null;
1473
1459
  /* O ouvinte de voz em segundo plano.
1474
1460
  Um por sessão: dois processos disputando o mesmo microfone dão um vazio
1475
1461
  intermitente que parece defeito de hardware e não é. */
1476
- let vozEmEspera = null;
1462
+ let vozEmEspera = null; // a sessão do /voice: { processo, falar, calar }
1463
+ /* Por onde a fala entra no terminal. Nasce no main(), junto da fila — é lá
1464
+ que moram o `busy`, a fila e o `ask` pendente. */
1465
+ let entradaDeVoz = null;
1477
1466
 
1478
1467
  async function runChat(promptForModel, userTextForMemory) {
1479
1468
  chatEmVoo = new AbortController();
@@ -1605,8 +1594,6 @@ async function rodarBash(text) {
1605
1594
  * escrever a linha, e a linha some da lista.
1606
1595
  */
1607
1596
  const RENOMEADOS = {
1608
- // o que abre a janela
1609
- app: 'rc',
1610
1597
  // a conta
1611
1598
  entrar: 'login', conectar: 'login',
1612
1599
  conta: 'account',
@@ -1851,7 +1838,9 @@ async function handleSlash(text) {
1851
1838
  case 'attach': {
1852
1839
  if (!rest) { console.log(c.err('Uso: /attach <arquivo>')); break; }
1853
1840
  try {
1854
- const content = fs.readFileSync(path.resolve(rest), 'utf-8');
1841
+ // Relativo à pasta de trabalho (a do /dir), como o /read — não ao
1842
+ // terminal de onde o PrimoCode foi aberto.
1843
+ const content = fs.readFileSync(path.resolve(projectDir(), rest), 'utf-8');
1855
1844
  state.attachedFiles.push({ path: rest, content });
1856
1845
  console.log(c.ok(`Anexado: ${rest} (${tools.fmtBytes(content.length)})`));
1857
1846
  refreshPrompt();
@@ -2237,46 +2226,32 @@ async function handleSlash(text) {
2237
2226
  }
2238
2227
 
2239
2228
  /* ── /voice ──────────────────────────────────────────────────────
2240
- * O modo voz abre uma janelinha com uma orb — não é uma janela de
2241
- * terminal. O aplicativo é Python (ver voz/primo_voz); o que acontece
2242
- * aqui é subir ele e responder ao que ele pedir.
2243
- *
2244
- * O cérebro NÃO vai junto: cada frase transcrita volta para o mesmo
2245
- * `runAct` do terminal, com as mesmas ferramentas, as mesmas skills e
2246
- * a mesma permissão de computador. Portar o laço do agente para o
2247
- * Python seria manter dois laços, e dois laços do mesmo agente é a
2248
- * garantia de que um deles está errado.
2229
+ * O ouvido e a boca são Python (voz/primo_voz); o cérebro é este
2230
+ * terminal. Ver entradaDeVoz no main() e lib/voz.js.
2249
2231
  */
2250
2232
  case 'voice': case 'voz': {
2251
- /* ── /voice PARA ─────────────────────────────────────────────
2252
- Encerrar é o primeiro caso tratado de propósito: quem quer
2253
- desligar o microfone quer desligar AGORA, e não deve esbarrar
2254
- em checagem de dependência nem em download de modelo. */
2233
+ /* ── O MODO VOZ É UMA FORMA DE DIGITAR ─────────────────────────
2234
+ Você fala; o que foi dito entra aqui como se tivesse sido
2235
+ digitado — a mesma fila, o mesmo recado no meio da tarefa, o
2236
+ mesmo roteador de conversa — e a resposta final sai em voz alta.
2237
+ O terminal continua seu: dá para falar e digitar ao mesmo tempo.
2238
+
2239
+ Desligar é o primeiro caso de propósito: quem quer fechar o
2240
+ microfone quer AGORA, sem esbarrar em checagem de dependência. */
2255
2241
  if (/^(parar|stop|off|sair)$/i.test((rest || '').trim())) {
2256
2242
  if (!vozEmEspera) { console.log(c.muted(' O modo voz não está ligado.')); break; }
2257
- try { vozEmEspera.kill(); } catch {}
2243
+ try { vozEmEspera.processo.kill(); } catch {}
2258
2244
  vozEmEspera = null;
2259
2245
  console.log(c.muted(' Modo voz desligado. O microfone fechou.'));
2260
2246
  break;
2261
2247
  }
2262
-
2263
2248
  if (vozEmEspera) {
2264
- console.log(c.muted(' O modo voz já está ouvindo. Diga "ei, primo".'));
2265
- console.log(c.dim(' /voice parar desliga.'));
2249
+ console.log(c.muted(' O modo voz já está ouvindo — é só falar.') + c.dim(' /voice parar desliga.'));
2266
2250
  break;
2267
2251
  }
2268
2252
 
2269
- /* ── PREPARAR TUDO, SEM PERGUNTAR ───────────────────────────
2270
- "Instale tudo que precisa, rode tudo que precisa." O que falta
2271
- (bibliotecas do sistema, pacotes do Python, o Whisper local e o
2272
- reconhecedor do "ei, primo") é instalado aqui, com uma linha de
2273
- progresso por etapa. Da segunda vez em diante, tudo já está lá
2274
- e isto leva um segundo. Ver `preparar` em lib/voz.js.
2275
-
2276
- O reconhecedor do nome é o que deixa o microfone aberto sem o
2277
- áudio sair da máquina: o nome é reconhecido AQUI. Se ele não
2278
- ficar pronto, o modo voz abre já ouvindo — nunca manda o dia
2279
- inteiro de áudio para o servidor. */
2253
+ // "Instale tudo que precisa, rode tudo que precisa." Da segunda vez
2254
+ // em diante tudo já está lá, e isto leva um segundo.
2280
2255
  const sp = new Spinner('preparando o modo voz').start();
2281
2256
  const prep = await vozModo.preparar((msg) => { sp.text = msg; });
2282
2257
  sp.stop();
@@ -2287,98 +2262,22 @@ async function handleSlash(text) {
2287
2262
  break;
2288
2263
  }
2289
2264
  for (const a of prep.avisos) console.log(c.dim(` ${a}`));
2290
- const py = prep.python;
2291
- const esperando = prep.esperando;
2292
-
2293
- console.log('');
2294
- console.log(' ' + c.bold(c.brand('Primo Code Voice')));
2295
- if (esperando) {
2296
- console.log(' ' + c.muted('Ouvindo em segundo plano. Diga ') + c.white('"ei, primo"') + c.muted(' e a orb aparece.'));
2297
- console.log(' ' + c.dim('Nada sai da máquina até você chamar · /voice parar desliga'));
2298
- } else {
2299
- console.log(' ' + c.muted('É só falar. A orb responde: ')
2300
- + c.dim('parada espera · pulsa ouvindo · respira pensando · brilha falando'));
2301
- console.log(' ' + c.dim('Falar por cima corta a resposta · /voice parar desliga'));
2302
- }
2303
- console.log('');
2304
2265
 
2305
2266
  const sessao = vozModo.abrir({
2306
- python: py,
2267
+ python: prep.python,
2307
2268
  servidor: state.config.server,
2308
2269
  token: state.config.token,
2309
2270
  voz: state.config.voz || 'antonio',
2310
- esperando,
2311
- mostrar: (texto, deErro) => {
2312
- if (deErro) console.log(' ' + c.dim(texto));
2313
- else console.log(' ' + c.brand('❯ ') + c.white(texto));
2314
- },
2315
- /* O agente de verdade. A instrução de FORMA vai na frente
2316
- porque resposta boa de ler é ruim de ouvir — o trabalho é o
2317
- mesmo, o jeito de contar é que muda. */
2318
- pensar: async (texto) => {
2319
- // Mesma memória do terminal: espera o resumo da rodada
2320
- // anterior e leva só ele, com o projeto atual.
2321
- await esperarResumo();
2322
- /* O mesmo roteador do terminal: cumprimento e pergunta vão
2323
- para a conversa, curta; só pedido de trabalho vai para o
2324
- agente. Antes TUDO ia para o agente — até um "oi". */
2325
- const escolha = escolherModo(texto, { ultimoModo: state.modoVoz });
2326
- state.modoVoz = escolha.modo;
2327
- if (escolha.modo === 'chat') {
2328
- let resposta = '';
2329
- try {
2330
- await api.requestChat(state.config.server, {
2331
- prompt: `${vozModo.COMO_CONVERSAR}\n\nA PESSOA DISSE: ${texto}`,
2332
- history: state.history.slice(-4),
2333
- model: state.config.model,
2334
- token: state.config.token,
2335
- }, (evt) => { if (evt.delta) resposta += evt.delta; });
2336
- } catch { /* sem resposta: a orb volta a ouvir */ }
2337
- resposta = resposta.trim();
2338
- state.history.push({ role: 'user', content: texto });
2339
- state.history.push({ role: 'assistant', content: resposta });
2340
- resumirDepois();
2341
- return resposta;
2342
- }
2343
- const plan = prepararPasta(texto);
2344
- // Pasta pendente (projeto que ainda vai nascer) é o ~/primocode: sem foto.
2345
- checkpoint.iniciar(texto, plan.pending ? null : plan.dir);
2346
- const resultado = await runAct(texto, {
2347
- instrucao: vozModo.COMO_FALAR,
2348
- server: state.config.server,
2349
- model: state.config.model,
2350
- effort: state.config.effort,
2351
- token: state.config.token,
2352
- history: memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto()),
2353
- projectDir: plan.dir,
2354
- projectName: plan.name || state.currentProject,
2355
- ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
2356
- autorizar: autorizarPorVoz,
2357
- controle: state.controle,
2358
- permissoes: state.config.permissoes,
2359
- });
2360
- checkpoint.encerrar();
2361
- const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
2362
- // A conversa é a MESMA do terminal: sair do modo voz e
2363
- // continuar digitando não pode começar do zero.
2364
- state.history.push({ role: 'user', content: texto });
2365
- state.history.push({ role: 'assistant', content: resposta });
2366
- resumirDepois();
2367
- const balanco = registro.resumo(resultado.registro, { colunas: process.stdout.columns || 80 });
2368
- if (balanco) console.log(balanco);
2369
- return resposta;
2271
+ aoPronto: () => {
2272
+ console.log(' ' + c.bold(c.brand('🎙 Modo voz ligado')) + c.muted(' — é só falar. A resposta sai em voz alta.'));
2273
+ console.log(' ' + c.dim('Falar por cima corta a voz · digitar continua valendo · /voice parar desliga'));
2370
2274
  },
2275
+ aoOuvir: (texto) => { if (entradaDeVoz) entradaDeVoz(texto); },
2276
+ mostrar: (texto) => console.log(' ' + c.dim(texto)),
2371
2277
  });
2372
-
2373
- /* O TERMINAL CONTINUA SEU.
2374
- Aqui NÃO se espera o fim. Era isso que obrigava a digitar
2375
- /voice antes de cada pedido: o comando bloqueava o prompt, e a
2376
- única forma de voltar a digitar era encerrar o modo voz. Agora
2377
- ele vive ao lado — a pessoa fala quando quiser e digita quando
2378
- quiser, sem desligar um para usar o outro. */
2379
- vozEmEspera = sessao.processo;
2278
+ vozEmEspera = sessao;
2380
2279
  sessao.pronto.then((r) => {
2381
- vozEmEspera = null;
2280
+ if (vozEmEspera === sessao) vozEmEspera = null;
2382
2281
  if (r && !r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
2383
2282
  });
2384
2283
  break;
@@ -2431,96 +2330,13 @@ async function handleSlash(text) {
2431
2330
  break;
2432
2331
  }
2433
2332
 
2434
- /* A janela. "Se mandar /rc pra ele, ele abre o aplicativo — para o
2435
- Mac e o Windows, já instalado junto com o npm, com as cores do
2436
- PrimoCode. Um chat estilo terminal para quem não quer o terminal."
2437
-
2438
- Uma janela por sessão: mandar /rc de novo traz a que já existe
2439
- para a frente em vez de abrir outra do lado. */
2440
- /* O nome antigo some da ajuda e do menu, mas continua respondendo —
2441
- avisando. Some de vez e quem tinha o dedo viciado bate num "comando
2442
- não existe" e acha que a atualização quebrou alguma coisa; o aviso
2443
- custa uma linha e ensina o nome novo na hora em que ele é
2444
- procurado, que é a única hora em que alguém lê. */
2445
- case 'rc': {
2446
- // `/rc --instalar` põe o PrimoCode no Dock, no menu Iniciar ou no
2447
- // menu de aplicativos, com o ícone do mascote. Uma vez só.
2448
- if (/^--?(instalar|install)$/.test(rest)) {
2449
- const r = require('../lib/instalar-app.js').instalar(VERSION);
2450
- console.log(r.ok
2451
- ? c.ok(' Instalado.') + c.muted(` Procure "PrimoCode" no ${r.onde}.`)
2452
- + '\n' + c.dim(` ${r.caminho}`)
2453
- : c.err(` Não deu: ${r.error}`));
2454
- break;
2455
- }
2456
- if (/^--?(desinstalar|uninstall)$/.test(rest)) {
2457
- const r = require('../lib/instalar-app.js').desinstalar();
2458
- console.log(r.ok ? c.ok(' Removido do sistema.') : c.err(` ${r.error}`));
2459
- break;
2460
- }
2461
- if (janelaAberta) {
2462
- // Já existe: reabre no MESMO endereço. O navegador traz a
2463
- // janela daquela URL para a frente em vez de fazer outra.
2464
- await appLib.abrirJanela(janelaAberta.url);
2465
- console.log(c.muted(' A janela já estava aberta — trouxe para a frente.'));
2466
- break;
2467
- }
2468
- const sp = new Spinner('abrindo a janela').start();
2469
- try {
2470
- janelaAberta = await appLib.abrir(opcoesDaJanela());
2471
- sp.stop();
2472
- const j = janelaAberta.janela || {};
2473
- console.log(c.ok(' Janela aberta.')
2474
- + c.muted(j.modo === 'janela' ? ` (${j.navegador})`
2475
- : j.modo === 'aba' ? ' (aba do navegador padrão)' : ''));
2476
- console.log(c.dim(` ${janelaAberta.url}`));
2477
- if (j.modo === 'nenhum') {
2478
- console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
2479
- }
2480
- console.log(c.muted(' O terminal continua funcionando: os dois mostram a mesma coisa.'));
2481
- // Oferecido UMA vez, e só a quem ainda não tem: repetir todo
2482
- // /rc viraria ruído para quem já instalou de propósito.
2483
- if (!require('../lib/instalar-app.js').instalado()) {
2484
- console.log(c.dim(' Quer o ícone no ') + c.dim(os.platform() === 'darwin' ? 'Dock' : 'menu Iniciar')
2485
- + c.dim('? ') + c.brand('/rc --instalar'));
2486
- }
2487
- } catch (e) {
2488
- sp.stop();
2489
- console.log(c.err(` Não subiu: ${e.message}`));
2490
- }
2333
+ /* A janela (/rc, /ide, `primo --app`) saiu na 10.0: o PrimoCode é
2334
+ o terminal. O nome antigo ainda responde, uma linha, para o dedo
2335
+ viciado não achar que a atualização quebrou. */
2336
+ case 'rc':
2337
+ case 'ide':
2338
+ console.log(c.muted(' A janela saiu na 10.0 — o PrimoCode agora é só o terminal.'));
2491
2339
  break;
2492
- }
2493
-
2494
- /* O /ide virou a MESMA janela do /rc, abrindo com a prévia do site
2495
- à direita. O editor de código saiu ("remove a IDE ou deixa
2496
- idêntico ao Codex"): quem não programa precisa ver o site
2497
- nascendo, não de um editor para quebrar o que o agente fez. O
2498
- comando continua existindo para o dedo viciado não bater em nada. */
2499
- case 'ide': {
2500
- if (janelaAberta) {
2501
- await appLib.abrirJanela(janelaAberta.urlPainel || janelaAberta.url);
2502
- console.log(c.ok(' Janela com a prévia aberta.') + c.muted(' (no mesmo servidor que já estava de pé)'));
2503
- break;
2504
- }
2505
- const sp = new Spinner('abrindo a janela').start();
2506
- try {
2507
- janelaAberta = await appLib.abrir(opcoesDaJanela({ abrirEm: 'ide' }));
2508
- sp.stop();
2509
- const j = janelaAberta.janela || {};
2510
- console.log(c.ok(' Janela aberta, com a prévia do site ao lado.')
2511
- + c.muted(j.modo === 'janela' ? ` (${j.navegador})`
2512
- : j.modo === 'aba' ? ' (aba do navegador padrão)' : ''));
2513
- console.log(c.dim(` ${janelaAberta.urlPainel}`));
2514
- if (j.modo === 'nenhum') {
2515
- console.log(c.warn(' Não consegui abrir sozinho — copie o endereço acima no navegador.'));
2516
- }
2517
- console.log(c.muted(` Pasta: ${shortPath(projectDir())} · o terminal continua funcionando, os dois mostram a mesma coisa.`));
2518
- } catch (e) {
2519
- sp.stop();
2520
- console.log(c.err(` Não subiu: ${e.message}`));
2521
- }
2522
- break;
2523
- }
2524
2340
 
2525
2341
  /* A conta do Conecta Primo, sem navegador: e-mail e senha aqui
2526
2342
  mesmo. O login inteiro mora em lib/conta.js; aqui é a tela dele. */
@@ -2801,6 +2617,19 @@ function conferirServidor() {
2801
2617
  }
2802
2618
 
2803
2619
  async function main() {
2620
+ /* `primocode video <op>`: o editor de vídeo por linha do tempo
2621
+ (lib/video/). Sai ANTES de tudo — sem sessão, sem pasta padrão —
2622
+ porque quem chama é o agente pelo run_shell, na pasta da gravação. */
2623
+ if (process.argv[2] === 'video') {
2624
+ process.exit(await require('../lib/video/cli.js').main(process.argv.slice(3)));
2625
+ }
2626
+ // `primocode mcp …`: os servidores MCP pelo run_shell (lib/mcp.js).
2627
+ if (process.argv[2] === 'skill' || process.argv[2] === 'skills') {
2628
+ process.exit(require('../lib/skills.js').main(process.argv.slice(3)));
2629
+ }
2630
+ if (process.argv[2] === 'mcp') {
2631
+ process.exit(await require('../lib/mcp.js').main(process.argv.slice(3)));
2632
+ }
2804
2633
  cfg.ensureStructure();
2805
2634
 
2806
2635
  // Padrão fixo: ~/primocode, não importa de onde o comando foi dado. Antes ele
@@ -2839,49 +2668,8 @@ async function main() {
2839
2668
  process.exit(0);
2840
2669
  }
2841
2670
 
2842
- /* `primo --app`: a janela SEM terminal.
2843
- *
2844
- * É por aqui que o aplicativo instalado entra. Quem clica no ícone do Dock
2845
- * ou do menu Iniciar não tem terminal nenhum — não há readline, não há
2846
- * stdin, e o processo precisa ficar de pé sozinho segurando a janela.
2847
- *
2848
- * O `/app` de dentro da sessão continua existindo e é outra coisa: ali há
2849
- * um terminal, e a janela espelha o que ele mostra. Os dois chamam a MESMA
2850
- * `appLib.abrir`, porque uma segunda montagem divergiria da primeira. */
2851
- if (args[0] === '--app') {
2852
- const instalador = require('../lib/instalar-app.js');
2853
- if (args[1] === '--instalar') {
2854
- const r = instalador.instalar(VERSION);
2855
- console.log(r.ok
2856
- ? c.ok(` Instalado.`) + c.muted(` Procure "PrimoCode" no ${r.onde}.`)
2857
- + '\n' + c.dim(` ${r.caminho}`)
2858
- : c.err(` Não deu: ${r.error}`));
2859
- process.exit(r.ok ? 0 : 1);
2860
- }
2861
- if (args[1] === '--desinstalar') {
2862
- const r = instalador.desinstalar();
2863
- console.log(r.ok ? c.ok(' Removido do sistema.') : c.err(` ${r.error}`));
2864
- process.exit(r.ok ? 0 : 1);
2865
- }
2866
-
2867
- const janela = await appLib.abrir(opcoesDaJanela());
2868
- janelaAberta = janela;
2869
- console.log(c.brand(` PrimoCode`) + c.muted(` ${janela.url.split('?')[0]}`));
2870
- // Sem readline não há nada segurando o laço de eventos além do
2871
- // servidor — e é ele que segura, porque está escutando. Fechar a
2872
- // janela NÃO derruba o processo de propósito: o navegador em modo app
2873
- // reabre a mesma URL, e um servidor morto deixaria a pessoa com uma
2874
- // aba de erro em vez do aplicativo.
2875
- return;
2876
- }
2877
-
2878
- /* `primo ide`: o terminal de sempre, com a janela (e a prévia) já aberta.
2879
- Só a palavra SOZINHA: `primo ide para gatos` continua sendo um pedido
2880
- ao agente (modo um-tiro), como sempre foi. */
2881
- const abrirIde = args.length === 1 && args[0].toLowerCase() === 'ide';
2882
-
2883
2671
  // Modo one-shot: primocode "crie um jogo de carro"
2884
- if (args.length && !args[0].startsWith('-') && !abrirIde) {
2672
+ if (args.length && !args[0].startsWith('-')) {
2885
2673
  const fileFlag = args.indexOf('-f');
2886
2674
  const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
2887
2675
  const promptText = words.join(' ');
@@ -2899,7 +2687,9 @@ async function main() {
2899
2687
  runAct próprio, e por isso ficava sem tudo o que vive lá: o plano em
2900
2688
  passos, a retomada automática, o @arquivo, o checkpoint do /desfazer. */
2901
2689
  state.semTerminal = true;
2690
+ global.__primocodeTarefa = promptText.slice(0, 60);
2902
2691
  await sendPrompt(promptText);
2692
+ global.__primocodeTarefa = null;
2903
2693
  process.exit(0);
2904
2694
  }
2905
2695
 
@@ -2914,6 +2704,9 @@ async function main() {
2914
2704
  comandos: comandosDoMenu,
2915
2705
  vazio: 'peça alguma coisa, ou / para os comandos',
2916
2706
  });
2707
+ // O spinner desenha dentro da caixa (lib/entrada.js mostrarSpinner).
2708
+ if (rl.tty) Spinner.caixa = rl;
2709
+ ui.titulo('PrimoCode');
2917
2710
 
2918
2711
  rl.prompt();
2919
2712
 
@@ -2962,7 +2755,7 @@ async function main() {
2962
2755
  acesa. Matar aqui é o que faz "fechei o terminal" significar
2963
2756
  "fechei tudo". */
2964
2757
  if (vozEmEspera) {
2965
- try { vozEmEspera.kill(); } catch { /* já morreu */ }
2758
+ try { vozEmEspera.processo.kill(); } catch { /* já morreu */ }
2966
2759
  vozEmEspera = null;
2967
2760
  }
2968
2761
  // O último turno pode ainda estar subindo — gravar é de propósito
@@ -2990,23 +2783,31 @@ async function main() {
2990
2783
  while (queue.length) {
2991
2784
  const text = queue.shift().trim();
2992
2785
  if (!text) continue;
2786
+ ui.titulo(`⠋ PrimoCode — ${text}`);
2993
2787
  try {
2994
2788
  if (ehBash(text)) await rodarBash(text);
2995
2789
  else if (isSlash(text)) await handleSlash(text);
2996
2790
  else {
2997
- // Com a janela aberta, o pedido digitado AQUI também vira
2998
- // balão lá — senão a janela mostraria a resposta de uma
2999
- // pergunta que ninguém fez.
3000
- if (janelaAberta && janelaAberta.pedidoDoTerminal) janelaAberta.pedidoDoTerminal(text);
3001
2791
  await sendPrompt(text);
3002
2792
  }
3003
2793
  } catch (e) {
3004
2794
  fala.problema(e);
3005
2795
  }
2796
+ // Pedido falado (ou tarefa que recebeu recado falado): a resposta
2797
+ // final sai em voz alta, resumida. O detalhe fica na tela.
2798
+ if (vozEmEspera && (deVoz.delete(text) || vozNaTarefa)) {
2799
+ vozNaTarefa = false;
2800
+ const ultima = state.history.slice().reverse().find((m) => m.role === 'assistant');
2801
+ if (ultima && ultima.content) vozEmEspera.falar(ultima.content);
2802
+ }
2803
+ // Recado que chegou tarde demais para a tarefa (ela já tinha
2804
+ // acabado): vira o próximo pedido, na ordem. Nada se perde.
2805
+ queue.unshift(...sobrasDaCaixa());
3006
2806
  }
3007
2807
  busy = false;
3008
2808
  state.trabalhando = false;
3009
2809
  if (rl.trabalhando) rl.trabalhando(false);
2810
+ ui.titulo('PrimoCode');
3010
2811
  // Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
3011
2812
  // — senão ela aparece antes do resultado do último pedido.
3012
2813
  if (closed) sayBye();
@@ -3017,12 +2818,41 @@ async function main() {
3017
2818
  if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
3018
2819
  // Abertura correndo: guarda em vez de mandar ao agente.
3019
2820
  if (segurando) { adiantadas.push(line); return; }
2821
+ /* Digitou com o agente no meio de uma tarefa: o recado entra NELA,
2822
+ depois do passo atual (ver a caixa em lib/act.js), em vez de
2823
+ esperar a tarefa acabar para virar um pedido que não sabe de nada.
2824
+ Comando (/…) e shell (!…) continuam indo para a fila. */
2825
+ const t = line.trim();
2826
+ if (busy && t && !isSlash(t) && !ehBash(t) && mandarDuranteATarefa(t)) {
2827
+ console.log(c.dim(' ↳ recado guardado — entra na tarefa depois do passo atual'));
2828
+ return;
2829
+ }
3020
2830
  queue.push(line);
3021
2831
  drain();
3022
2832
  });
3023
2833
 
3024
2834
  rl.on('close', () => { closed = true; if (!busy) sayBye(); });
3025
2835
 
2836
+ /* A FALA ENTRA AQUI, pelo mesmo caminho da linha digitada: responde a
2837
+ pergunta pendente (a autorização, por exemplo — "pode" é resposta), vira
2838
+ recado se ele está no meio de uma tarefa, ou vira pedido na fila. O que
2839
+ veio falado é marcado para a resposta sair em voz alta. */
2840
+ const deVoz = new Set();
2841
+ let vozNaTarefa = false;
2842
+ entradaDeVoz = (texto) => {
2843
+ console.log(' ' + c.brand('🎙 ') + c.white(texto));
2844
+ if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(texto); return; }
2845
+ if (segurando) { adiantadas.push(texto); return; }
2846
+ if (busy && !isSlash(texto) && mandarDuranteATarefa(texto)) {
2847
+ vozNaTarefa = true;
2848
+ console.log(c.dim(' ↳ recado guardado — entra na tarefa depois do passo atual'));
2849
+ return;
2850
+ }
2851
+ deVoz.add(texto);
2852
+ queue.push(texto);
2853
+ drain();
2854
+ };
2855
+
3026
2856
  /* A ABERTURA — e ela roda AQUI, depois de `line` e `close` existirem.
3027
2857
  *
3028
2858
  * Estava antes deles. O `await` segurava o resto do main(), e tudo o que
@@ -3064,9 +2894,6 @@ async function main() {
3064
2894
  // O que sobrou de adiantado não era resposta de nada: vira comando,
3065
2895
  // na ordem em que foi digitado. Nada se perde, só esperou a vez.
3066
2896
  segurando = false;
3067
- // Depois da abertura (e do login dela): a janela sem conta abriria
3068
- // mandando pedidos que o servidor recusa.
3069
- if (abrirIde) queue.push('/ide');
3070
2897
  while (adiantadas.length) queue.push(adiantadas.shift());
3071
2898
  if (queue.length) drain();
3072
2899
  else if (!closed) rl.prompt();