primocode 9.9.0 → 10.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 (63) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/ende.js +2 -0
  12. package/lib/estudio/estilo.js +51 -0
  13. package/lib/estudio/glifos.js +215 -0
  14. package/lib/estudio/index.js +63 -11
  15. package/lib/estudio/kit/base.jsx +285 -0
  16. package/lib/estudio/kit/index.jsx +11 -0
  17. package/lib/estudio/kit/marca.jsx +741 -0
  18. package/lib/estudio/kit/vox.jsx +357 -0
  19. package/lib/estudio/kit.js +72 -0
  20. package/lib/estudio/lancamento.js +413 -0
  21. package/lib/estudio/morfar.js +191 -0
  22. package/lib/estudio/motion.js +333 -0
  23. package/lib/estudio/plano.js +54 -7
  24. package/lib/estudio/remotion.js +167 -52
  25. package/lib/estudio/ritmo.js +77 -0
  26. package/lib/estudio/sons-leves.js +62 -0
  27. package/lib/estudio/vox.js +385 -0
  28. package/lib/fala.js +1 -94
  29. package/lib/pipeline.js +55 -56
  30. package/lib/repeticao.js +3 -1
  31. package/lib/skills.js +8 -2
  32. package/lib/snapshot.js +111 -0
  33. package/lib/tools.js +79 -20
  34. package/lib/ui.js +13 -6
  35. package/lib/video/analise.js +89 -0
  36. package/lib/video/cli.js +74 -0
  37. package/lib/video/delta.js +90 -0
  38. package/lib/video/edits.js +379 -0
  39. package/lib/video/filters.js +247 -0
  40. package/lib/video/ops.js +322 -0
  41. package/lib/video/projeto.js +133 -0
  42. package/lib/video/render.js +221 -0
  43. package/lib/video/texto.js +142 -0
  44. package/lib/video/timeline.js +310 -0
  45. package/lib/video/transcript.js +124 -0
  46. package/lib/voz.js +75 -223
  47. package/package.json +2 -3
  48. package/skills/motion-de-primeira/SKILL.md +21 -1
  49. package/studio/texto.py +208 -0
  50. package/voz/primo_voz/__main__.py +96 -236
  51. package/voz/primo_voz/transcrever.py +31 -1
  52. package/app/ansi.js +0 -55
  53. package/app/app.css +0 -500
  54. package/app/app.js +0 -1017
  55. package/app/icone.svg +0 -6
  56. package/app/index.html +0 -220
  57. package/app/primo-marca.js +0 -192
  58. package/lib/app.js +0 -615
  59. package/lib/icone.js +0 -263
  60. package/lib/instalar-app.js +0 -231
  61. package/lib/painel.js +0 -291
  62. package/voz/primo_voz/desperta.py +0 -195
  63. 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');
@@ -104,59 +103,6 @@ const state = {
104
103
  workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
105
104
  };
106
105
 
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
106
  const HELP = `
161
107
  ${c.bold('Comandos')}
162
108
  ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
@@ -179,8 +125,6 @@ ${c.bold('Comandos')}
179
125
  ${c.brand('/abrir')} abre o projeto: o servidor de pé, ou a index.html
180
126
  ${c.brand('/copiar')} copia a última resposta
181
127
  ${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
128
  ${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
185
129
  ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
186
130
  ${c.brand('/logout')} desconecta este computador da conta
@@ -237,14 +181,12 @@ const SLASH_COMANDOS = [
237
181
  ['/abrir', 'abre o projeto atual: o servidor de pé, ou a index.html'],
238
182
  ['/copiar', 'copia a última resposta'],
239
183
  ['/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
184
  ['/login', 'entra na sua conta Conecta Primo'],
243
185
  ['/account', 'sua conta, plano e computadores conectados'],
244
186
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
245
187
  ['/logout', 'desconecta este computador da conta'],
246
188
  ['!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'],
189
+ ['/voice', 'liga o microfone: você fala, ele faz, e a resposta sai em voz alta'],
248
190
  ['/voice parar', 'desliga o modo voz e fecha o microfone'],
249
191
  ['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
250
192
  ['/marketplace', 'a tela das skills: navegue, ligue, crie'],
@@ -669,7 +611,12 @@ function deriveProjectName(text) {
669
611
  * ("isso", "esse", "nele"). Todo o resto ("continua", "muda a cor", "agora
670
612
  * põe fases") fica. */
671
613
  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/;
614
+ /* "crie UM site…" e também "crie O SITE da barbearia": o artigo definido com
615
+ o tipo da coisa é projeto novo com nome (medido em 29/09: "crie o site
616
+ bench-barbearia" nasceu DENTRO do projeto que estava aberto). Se for o
617
+ mesmo assunto do projeto atual, a comparação de assunto em pedeProjetoNovo
618
+ segura como continuação. */
619
+ 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
620
  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
621
 
675
622
  /* "crie uma nova seção", "cria um botão", "faz uma tela de login": é um
@@ -871,8 +818,15 @@ async function autorizarControle(oQueVaiFazer, { primeira, tipo } = {}) {
871
818
  + c.dim('2') + c.muted(' esta sessão inteira ')
872
819
  + c.dim('Enter') + c.muted(' não'));
873
820
 
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)) {
821
+ // Sem pontuação: a resposta pode vir FALADA (/voice), e o Whisper devolve
822
+ // "Sim." e "Pode, sim." — que também são sim.
823
+ const dito = String(await ask(' ' + c.brand('❯ ') + c.muted('1, 2 ou Enter '))).trim().toLowerCase()
824
+ .replace(/[.!?,;:…]+/g, ' ').replace(/\s+/g, ' ').trim();
825
+ if (/sess|sempre|tudo|todas/.test(dito) && dito !== '1') {
826
+ console.log(' ' + c.ok('✔ autorizado') + c.dim(' — esta sessão inteira. Fechar o PrimoCode esquece.'));
827
+ return 'sessao';
828
+ }
829
+ if (dito === '1' || /^(s|sim|ok|pode|agora|y|yes|claro|vai)\b/.test(dito)) {
876
830
  console.log(' ' + c.ok('✔ autorizado') + c.dim(' — só esta ação'));
877
831
  return 'agora';
878
832
  }
@@ -884,28 +838,6 @@ async function autorizarControle(oQueVaiFazer, { primeira, tipo } = {}) {
884
838
  return 'nao';
885
839
  }
886
840
 
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
841
  /* ── A TELA DO MARKETPLACE ───────────────────────────────────────────────
910
842
  * O desenho e as teclas vivem em lib/marketplace.js, que é função pura. O que
911
843
  * mora aqui é o que precisa do terminal: tirar a caixa de digitar da frente,
@@ -1418,7 +1350,7 @@ function resumirDepois() {
1418
1350
  atual.then(() => { if (state.resumindo === atual) state.resumindo = null; });
1419
1351
  }
1420
1352
 
1421
- async function esperarResumo(tetoMs = 15000) {
1353
+ async function esperarResumo(tetoMs = 6000) {
1422
1354
  if (!state.resumindo) return;
1423
1355
  await Promise.race([state.resumindo, new Promise((ok) => setTimeout(ok, tetoMs))]);
1424
1356
  }
@@ -1473,7 +1405,10 @@ let chatEmVoo = null;
1473
1405
  /* O ouvinte de voz em segundo plano.
1474
1406
  Um por sessão: dois processos disputando o mesmo microfone dão um vazio
1475
1407
  intermitente que parece defeito de hardware e não é. */
1476
- let vozEmEspera = null;
1408
+ let vozEmEspera = null; // a sessão do /voice: { processo, falar, calar }
1409
+ /* Por onde a fala entra no terminal. Nasce no main(), junto da fila — é lá
1410
+ que moram o `busy`, a fila e o `ask` pendente. */
1411
+ let entradaDeVoz = null;
1477
1412
 
1478
1413
  async function runChat(promptForModel, userTextForMemory) {
1479
1414
  chatEmVoo = new AbortController();
@@ -1605,8 +1540,6 @@ async function rodarBash(text) {
1605
1540
  * escrever a linha, e a linha some da lista.
1606
1541
  */
1607
1542
  const RENOMEADOS = {
1608
- // o que abre a janela
1609
- app: 'rc',
1610
1543
  // a conta
1611
1544
  entrar: 'login', conectar: 'login',
1612
1545
  conta: 'account',
@@ -1851,7 +1784,9 @@ async function handleSlash(text) {
1851
1784
  case 'attach': {
1852
1785
  if (!rest) { console.log(c.err('Uso: /attach <arquivo>')); break; }
1853
1786
  try {
1854
- const content = fs.readFileSync(path.resolve(rest), 'utf-8');
1787
+ // Relativo à pasta de trabalho (a do /dir), como o /read — não ao
1788
+ // terminal de onde o PrimoCode foi aberto.
1789
+ const content = fs.readFileSync(path.resolve(projectDir(), rest), 'utf-8');
1855
1790
  state.attachedFiles.push({ path: rest, content });
1856
1791
  console.log(c.ok(`Anexado: ${rest} (${tools.fmtBytes(content.length)})`));
1857
1792
  refreshPrompt();
@@ -2237,46 +2172,32 @@ async function handleSlash(text) {
2237
2172
  }
2238
2173
 
2239
2174
  /* ── /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.
2175
+ * O ouvido e a boca são Python (voz/primo_voz); o cérebro é este
2176
+ * terminal. Ver entradaDeVoz no main() e lib/voz.js.
2249
2177
  */
2250
2178
  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. */
2179
+ /* ── O MODO VOZ É UMA FORMA DE DIGITAR ─────────────────────────
2180
+ Você fala; o que foi dito entra aqui como se tivesse sido
2181
+ digitado — a mesma fila, o mesmo recado no meio da tarefa, o
2182
+ mesmo roteador de conversa — e a resposta final sai em voz alta.
2183
+ O terminal continua seu: dá para falar e digitar ao mesmo tempo.
2184
+
2185
+ Desligar é o primeiro caso de propósito: quem quer fechar o
2186
+ microfone quer AGORA, sem esbarrar em checagem de dependência. */
2255
2187
  if (/^(parar|stop|off|sair)$/i.test((rest || '').trim())) {
2256
2188
  if (!vozEmEspera) { console.log(c.muted(' O modo voz não está ligado.')); break; }
2257
- try { vozEmEspera.kill(); } catch {}
2189
+ try { vozEmEspera.processo.kill(); } catch {}
2258
2190
  vozEmEspera = null;
2259
2191
  console.log(c.muted(' Modo voz desligado. O microfone fechou.'));
2260
2192
  break;
2261
2193
  }
2262
-
2263
2194
  if (vozEmEspera) {
2264
- console.log(c.muted(' O modo voz já está ouvindo. Diga "ei, primo".'));
2265
- console.log(c.dim(' /voice parar desliga.'));
2195
+ console.log(c.muted(' O modo voz já está ouvindo — é só falar.') + c.dim(' /voice parar desliga.'));
2266
2196
  break;
2267
2197
  }
2268
2198
 
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. */
2199
+ // "Instale tudo que precisa, rode tudo que precisa." Da segunda vez
2200
+ // em diante tudo já está lá, e isto leva um segundo.
2280
2201
  const sp = new Spinner('preparando o modo voz').start();
2281
2202
  const prep = await vozModo.preparar((msg) => { sp.text = msg; });
2282
2203
  sp.stop();
@@ -2287,98 +2208,22 @@ async function handleSlash(text) {
2287
2208
  break;
2288
2209
  }
2289
2210
  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
2211
 
2305
2212
  const sessao = vozModo.abrir({
2306
- python: py,
2213
+ python: prep.python,
2307
2214
  servidor: state.config.server,
2308
2215
  token: state.config.token,
2309
2216
  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;
2217
+ aoPronto: () => {
2218
+ console.log(' ' + c.bold(c.brand('🎙 Modo voz ligado')) + c.muted(' — é só falar. A resposta sai em voz alta.'));
2219
+ console.log(' ' + c.dim('Falar por cima corta a voz · digitar continua valendo · /voice parar desliga'));
2370
2220
  },
2221
+ aoOuvir: (texto) => { if (entradaDeVoz) entradaDeVoz(texto); },
2222
+ mostrar: (texto) => console.log(' ' + c.dim(texto)),
2371
2223
  });
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;
2224
+ vozEmEspera = sessao;
2380
2225
  sessao.pronto.then((r) => {
2381
- vozEmEspera = null;
2226
+ if (vozEmEspera === sessao) vozEmEspera = null;
2382
2227
  if (r && !r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
2383
2228
  });
2384
2229
  break;
@@ -2431,96 +2276,13 @@ async function handleSlash(text) {
2431
2276
  break;
2432
2277
  }
2433
2278
 
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
- }
2491
- 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
- }
2279
+ /* A janela (/rc, /ide, `primo --app`) saiu na 10.0: o PrimoCode é
2280
+ o terminal. O nome antigo ainda responde, uma linha, para o dedo
2281
+ viciado não achar que a atualização quebrou. */
2282
+ case 'rc':
2283
+ case 'ide':
2284
+ console.log(c.muted(' A janela saiu na 10.0 — o PrimoCode agora é só o terminal.'));
2522
2285
  break;
2523
- }
2524
2286
 
2525
2287
  /* A conta do Conecta Primo, sem navegador: e-mail e senha aqui
2526
2288
  mesmo. O login inteiro mora em lib/conta.js; aqui é a tela dele. */
@@ -2801,6 +2563,12 @@ function conferirServidor() {
2801
2563
  }
2802
2564
 
2803
2565
  async function main() {
2566
+ /* `primocode video <op>`: o editor de vídeo por linha do tempo
2567
+ (lib/video/). Sai ANTES de tudo — sem sessão, sem pasta padrão —
2568
+ porque quem chama é o agente pelo run_shell, na pasta da gravação. */
2569
+ if (process.argv[2] === 'video') {
2570
+ process.exit(await require('../lib/video/cli.js').main(process.argv.slice(3)));
2571
+ }
2804
2572
  cfg.ensureStructure();
2805
2573
 
2806
2574
  // Padrão fixo: ~/primocode, não importa de onde o comando foi dado. Antes ele
@@ -2839,49 +2607,8 @@ async function main() {
2839
2607
  process.exit(0);
2840
2608
  }
2841
2609
 
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
2610
  // Modo one-shot: primocode "crie um jogo de carro"
2884
- if (args.length && !args[0].startsWith('-') && !abrirIde) {
2611
+ if (args.length && !args[0].startsWith('-')) {
2885
2612
  const fileFlag = args.indexOf('-f');
2886
2613
  const words = fileFlag >= 0 ? args.slice(0, fileFlag) : args;
2887
2614
  const promptText = words.join(' ');
@@ -2962,7 +2689,7 @@ async function main() {
2962
2689
  acesa. Matar aqui é o que faz "fechei o terminal" significar
2963
2690
  "fechei tudo". */
2964
2691
  if (vozEmEspera) {
2965
- try { vozEmEspera.kill(); } catch { /* já morreu */ }
2692
+ try { vozEmEspera.processo.kill(); } catch { /* já morreu */ }
2966
2693
  vozEmEspera = null;
2967
2694
  }
2968
2695
  // O último turno pode ainda estar subindo — gravar é de propósito
@@ -2994,15 +2721,21 @@ async function main() {
2994
2721
  if (ehBash(text)) await rodarBash(text);
2995
2722
  else if (isSlash(text)) await handleSlash(text);
2996
2723
  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
2724
  await sendPrompt(text);
3002
2725
  }
3003
2726
  } catch (e) {
3004
2727
  fala.problema(e);
3005
2728
  }
2729
+ // Pedido falado (ou tarefa que recebeu recado falado): a resposta
2730
+ // final sai em voz alta, resumida. O detalhe fica na tela.
2731
+ if (vozEmEspera && (deVoz.delete(text) || vozNaTarefa)) {
2732
+ vozNaTarefa = false;
2733
+ const ultima = state.history.slice().reverse().find((m) => m.role === 'assistant');
2734
+ if (ultima && ultima.content) vozEmEspera.falar(ultima.content);
2735
+ }
2736
+ // Recado que chegou tarde demais para a tarefa (ela já tinha
2737
+ // acabado): vira o próximo pedido, na ordem. Nada se perde.
2738
+ queue.unshift(...sobrasDaCaixa());
3006
2739
  }
3007
2740
  busy = false;
3008
2741
  state.trabalhando = false;
@@ -3017,12 +2750,41 @@ async function main() {
3017
2750
  if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
3018
2751
  // Abertura correndo: guarda em vez de mandar ao agente.
3019
2752
  if (segurando) { adiantadas.push(line); return; }
2753
+ /* Digitou com o agente no meio de uma tarefa: o recado entra NELA,
2754
+ depois do passo atual (ver a caixa em lib/act.js), em vez de
2755
+ esperar a tarefa acabar para virar um pedido que não sabe de nada.
2756
+ Comando (/…) e shell (!…) continuam indo para a fila. */
2757
+ const t = line.trim();
2758
+ if (busy && t && !isSlash(t) && !ehBash(t) && mandarDuranteATarefa(t)) {
2759
+ console.log(c.dim(' ↳ recado guardado — entra na tarefa depois do passo atual'));
2760
+ return;
2761
+ }
3020
2762
  queue.push(line);
3021
2763
  drain();
3022
2764
  });
3023
2765
 
3024
2766
  rl.on('close', () => { closed = true; if (!busy) sayBye(); });
3025
2767
 
2768
+ /* A FALA ENTRA AQUI, pelo mesmo caminho da linha digitada: responde a
2769
+ pergunta pendente (a autorização, por exemplo — "pode" é resposta), vira
2770
+ recado se ele está no meio de uma tarefa, ou vira pedido na fila. O que
2771
+ veio falado é marcado para a resposta sair em voz alta. */
2772
+ const deVoz = new Set();
2773
+ let vozNaTarefa = false;
2774
+ entradaDeVoz = (texto) => {
2775
+ console.log(' ' + c.brand('🎙 ') + c.white(texto));
2776
+ if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(texto); return; }
2777
+ if (segurando) { adiantadas.push(texto); return; }
2778
+ if (busy && !isSlash(texto) && mandarDuranteATarefa(texto)) {
2779
+ vozNaTarefa = true;
2780
+ console.log(c.dim(' ↳ recado guardado — entra na tarefa depois do passo atual'));
2781
+ return;
2782
+ }
2783
+ deVoz.add(texto);
2784
+ queue.push(texto);
2785
+ drain();
2786
+ };
2787
+
3026
2788
  /* A ABERTURA — e ela roda AQUI, depois de `line` e `close` existirem.
3027
2789
  *
3028
2790
  * Estava antes deles. O `await` segurava o resto do main(), e tudo o que
@@ -3064,9 +2826,6 @@ async function main() {
3064
2826
  // O que sobrou de adiantado não era resposta de nada: vira comando,
3065
2827
  // na ordem em que foi digitado. Nada se perde, só esperou a vez.
3066
2828
  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
2829
  while (adiantadas.length) queue.push(adiantadas.shift());
3071
2830
  if (queue.length) drain();
3072
2831
  else if (!closed) rl.prompt();