primocode 10.14.1 → 10.15.1

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.
@@ -0,0 +1,19 @@
1
+ Robô 3D da tela do Primo (voz/primo_voz/vendor/RobotExpressive.glb)
2
+
3
+ Modelo: RobotExpressive, de Tomás Laulhé (https://www.patreon.com/quaternius). CC0 1.0 (domínio público).
4
+ Modificações de Don McCurdy: três morph targets de expressão, conversão com FBX2GLTF, materiais deduplicados.
5
+ Fonte: https://github.com/mrdoob/three.js/tree/r170/examples/models/gltf/RobotExpressive (README conferido na tag r170).
6
+ Aqui as cores dos materiais são trocadas em tempo de execução (branco/grafite + ciano); o arquivo .glb é o original.
7
+
8
+ three.js r170 + GLTFLoader (voz/primo_voz/vendor/three-robo.min.js, bundle mínimo gerado com esbuild)
9
+ Licença MIT, Copyright 2010-2024 three.js authors. Texto completo: voz/primo_voz/vendor/three.LICENSE.txt
10
+
11
+ --- Robô do Spline (voz.json: "robo": "spline", o padrão; "expressive" volta ao RobotExpressive acima) ---
12
+
13
+ Cena: "AI Robotics Landing Page" (template da comunidade v0/Spline)
14
+ https://v0.app/templates/ai-robotics-landing-page-iArz1CoDHnf
15
+ https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode (baixada em tempo de execução, precisa de internet; não vai no repo)
16
+ Licença da cena: [VERIFICAR] — confirmar com o autor/Spline antes de uso comercial. Uso atual: pedido do Joel, uso interno.
17
+ Runtime: @splinetool/runtime 1.12.95 (voz/primo_voz/vendor/spline-runtime.min.js, bundle IIFE gerado com esbuild; local, sem CDN).
18
+ O pacote npm não declara campo "license" [VERIFICAR] — o Joel informou MIT; conferir no repositório splinetool antes de redistribuir.
19
+ Sem internet ou cena lenta (> 6 s): cai no RobotExpressive (CC0).
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.14.0
1
+ # PrimoCode v10.15.1
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v10.14.0
12
+ ▐▛███▜▌ Primo Code v10.15.1
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -17,6 +17,10 @@ está no limite de pedidos, o próximo atende. Você não vê o erro, e não pag
17
17
  Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
18
18
  veja `/mascot` e `/name`.
19
19
 
20
+ ## Primo (voz)
21
+
22
+ `/voice` abre a tela do Primo: agenda, clima, música, máquina e um robô 3D no centro. Diga **"Primo"** para chamar; ele fala frase a frase e você pode interromper. Na primeira vez cria `~/.primocode/voz-venv` sozinho (`primocode doutor` mostra o estado). Robô offline: `"robo":"expressive"` em `~/.primocode/voz.json`.
23
+
20
24
  ## Novidades da 10.14
21
25
 
22
26
  - **Vídeo de motion de ponta a ponta** (EffectCraft, 68 receitas, voz, trilha, prévia e editor por comando). Detalhes no `CHANGELOG.md`.
package/bin/primocode.js CHANGED
@@ -212,7 +212,9 @@ const SLASH_COMANDOS = [
212
212
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
213
213
  ['/logout', 'desconecta este computador da conta'],
214
214
  ['!comando', 'roda direto no shell, sem passar pelo agente (o campo fica âmbar)'],
215
- ['/voice', 'liga o microfone: você fala, ele faz, e a resposta sai em voz alta'],
215
+ ['/voice', 'liga o Primo: diga "Primo, ..."; ele faz e responde em voz alta, com a tela dele (robô, agenda, clima, música)'],
216
+ ['/voice painel', 'só o painel do Primo (ESC esconde)'],
217
+ ['/voice nome <Nome>', 'dá outro nome ao assistente (vira a palavra de ativação)'],
216
218
  ['/voice parar', 'desliga o modo voz e fecha o microfone'],
217
219
  ['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
218
220
  ['/marketplace', 'a tela das skills: navegue, ligue, crie'],
@@ -1619,6 +1621,7 @@ let chatEmVoo = null;
1619
1621
  /* O ouvinte de voz em segundo plano.
1620
1622
  Um por sessão: dois processos disputando o mesmo microfone dão um vazio
1621
1623
  intermitente que parece defeito de hardware e não é. */
1624
+ let vozPainel = null; // /voice painel: só o HUD
1622
1625
  let vozEmEspera = null; // a sessão do /voice: { processo, falar, calar }
1623
1626
  /* Por onde a fala entra no terminal. Nasce no main(), junto da fila — é lá
1624
1627
  que moram o `busy`, a fila e o `ask` pendente. */
@@ -2474,8 +2477,26 @@ async function handleSlash(text) {
2474
2477
 
2475
2478
  Desligar é o primeiro caso de propósito: quem quer fechar o
2476
2479
  microfone quer AGORA, sem esbarrar em checagem de dependência. */
2477
- if (/^(parar|stop|off|sair)$/i.test((rest || '').trim())) {
2478
- if (!vozEmEspera) { console.log(c.muted(' O modo voz não está ligado.')); break; }
2480
+ const argVoz = (rest || '').trim();
2481
+ // Só o painel (HUD em tela cheia no Mac; quadro no terminal fora dele).
2482
+ if (/^painel$/i.test(argVoz)) {
2483
+ const py = vozModo.python();
2484
+ if (!py) { console.log(c.err(' O painel precisa do Python 3.')); break; }
2485
+ if (vozPainel) { try { vozPainel.processo.kill(); } catch {} vozPainel = null; }
2486
+ vozPainel = vozModo.abrirPainel({ python: py });
2487
+ console.log(c.muted(vozPainel.tela ? ' Tela do Primo aberta — ESC esconde.' : ' Sem o painel gráfico aqui: rode `primocode voz painel` num terminal separado.'));
2488
+ break;
2489
+ }
2490
+ // /voice nome <Nome>: a pessoa dá o nome ao assistente (e a palavra de ativação passa a ser ele).
2491
+ const mNome = argVoz.match(/^nome\s+(.{2,20})$/i);
2492
+ if (mNome) {
2493
+ const salvo = vozModo.definirNome(mNome[1]);
2494
+ console.log(salvo ? c.ok(` Fechado: o assistente agora se chama ${salvo}. Vale na próxima vez que ligar o /voice.`) : c.err(' Nome inválido (1 ou 2 palavras, até 20 letras).'));
2495
+ break;
2496
+ }
2497
+ if (/^(parar|stop|off|sair)$/i.test(argVoz)) {
2498
+ if (vozPainel) { try { vozPainel.processo.kill(); } catch {} vozPainel = null; }
2499
+ if (!vozEmEspera) { console.log(c.muted(vozPainel === null ? ' Modo voz e painel fechados.' : ' O modo voz não está ligado.')); break; }
2479
2500
  try { vozEmEspera.processo.kill(); } catch {}
2480
2501
  vozEmEspera = null;
2481
2502
  console.log(c.muted(' Modo voz desligado. O microfone fechou.'));
@@ -2498,6 +2519,7 @@ async function handleSlash(text) {
2498
2519
  break;
2499
2520
  }
2500
2521
  for (const a of prep.avisos) console.log(c.dim(` ${a}`));
2522
+ if (process.platform === 'darwin') { try { vozModo.instalarApp({ python: prep.python }); } catch { /* o app é bônus */ } }
2501
2523
 
2502
2524
  const sessao = vozModo.abrir({
2503
2525
  python: prep.python,
@@ -2508,10 +2530,12 @@ async function handleSlash(text) {
2508
2530
  console.log(' ' + c.bold(c.brand('🎙 Modo voz ligado')) + c.muted(' — é só falar. A resposta sai em voz alta.'));
2509
2531
  console.log(' ' + c.dim('Falar por cima corta a voz · digitar continua valendo · /voice parar desliga'));
2510
2532
  },
2533
+ aoParar: () => { try { require('../lib/act.js').interromper(); } catch { /* nada rodando */ } },
2511
2534
  aoOuvir: (texto) => { if (entradaDeVoz) entradaDeVoz(texto); },
2512
2535
  mostrar: (texto) => console.log(' ' + c.dim(texto)),
2513
2536
  });
2514
2537
  vozEmEspera = sessao;
2538
+ require('../lib/voz-ponte').escutar((nome, args) => sessao.ferramenta(nome, args));
2515
2539
  sessao.pronto.then((r) => {
2516
2540
  if (vozEmEspera === sessao) vozEmEspera = null;
2517
2541
  if (r && !r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
@@ -2883,6 +2907,32 @@ async function main() {
2883
2907
  process.exit(require('../lib/skills.js').main(process.argv.slice(3)));
2884
2908
  }
2885
2909
  // `primocode planos`: a tabela comparativa, sem abrir a sessão.
2910
+ // `primocode voz nome <Nome>` / `primocode voz painel`: o Primo sem abrir a sessão.
2911
+ if (process.argv[2] === 'voz') {
2912
+ const vz = require('../lib/voz.js');
2913
+ const [sub, ...resto] = process.argv.slice(3);
2914
+ if (sub === 'nome') {
2915
+ const n = vz.definirNome(resto.join(' '));
2916
+ console.log(n ? `Fechado: o assistente agora se chama ${n}.` : 'Uso: primocode voz nome <Nome> (1 ou 2 palavras)');
2917
+ process.exit(n ? 0 : 1);
2918
+ }
2919
+ if (sub === 'painel') {
2920
+ const py = vz.python();
2921
+ if (!py) { console.log('O painel precisa do Python 3.'); process.exit(1); }
2922
+ const r = vz.abrirPainel({ python: py });
2923
+ r.processo.on('close', () => process.exit(0));
2924
+ return;
2925
+ }
2926
+ if (sub === 'instalar-app') {
2927
+ const py = vz.python();
2928
+ if (!py) { console.log('O app precisa do Python 3 (rode /voice uma vez para preparar o ambiente).'); process.exit(1); }
2929
+ const r = vz.instalarApp({ python: py, dock: resto.includes('--dock') });
2930
+ console.log(r.ok ? `Primo.app instalado em ${r.app}. Abra pelo Launchpad, Spotlight ou Finder.` : `Não deu: ${r.erro || 'veja o log'}`);
2931
+ process.exit(r.ok ? 0 : 1);
2932
+ }
2933
+ console.log('Uso: primocode voz nome <Nome> | primocode voz painel | primocode voz instalar-app [--dock] (o resto é /voice dentro da sessão)');
2934
+ process.exit(0);
2935
+ }
2886
2936
  if (['doutor', 'doctor', 'preparar'].includes(process.argv[2])) {
2887
2937
  process.exit(await require('../lib/preparar.js').main(process.argv.slice(2)));
2888
2938
  }
@@ -3129,6 +3179,7 @@ async function main() {
3129
3179
  }
3130
3180
  } catch (e) {
3131
3181
  fala.problema(e);
3182
+ if (vozEmEspera && vozEmEspera.estado) vozEmEspera.estado('erro');
3132
3183
  }
3133
3184
  // Pedido falado (ou tarefa que recebeu recado falado): a resposta
3134
3185
  // final sai em voz alta, resumida. O detalhe fica na tela.
package/lib/act.js CHANGED
@@ -846,6 +846,7 @@ async function runAct(userPrompt, opts) {
846
846
  let paradoPorTravar = null;
847
847
  /* Anota ANTES de agir (lib/auditoria.js): recusado também fica no livro. */
848
848
  const anotar = (tc, decisao, motivo) => {
849
+ if (decisao !== 'recusada' && decisao !== 'negada') require('./voz-ponte').ferramenta(tc.name, tc.arguments);
849
850
  const id = auditoria.antes({ agente: rotulo, ferramenta: tc.name, args: tc.arguments, decisao, motivo });
850
851
  const falhou = auditoria.problema();
851
852
  if (falhou) console.log(c.warn(`⚠ ${falhou}`));
package/lib/conta.js CHANGED
@@ -213,6 +213,7 @@ async function entrar({ email, senha } = {}, op = {}) {
213
213
  }
214
214
 
215
215
  function abrirNoNavegador(url) {
216
+ if (process.env.PRIMOCODE_SEM_NAVEGADOR === '1') return false; // app do Primo (voz): navegador só com clique explícito
216
217
  const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
217
218
  : process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', url]]
218
219
  : ['xdg-open', [url]];
@@ -156,6 +156,7 @@ function registrar({ id, assunto, caminho, formato, duracao, assinatura, entrega
156
156
  }
157
157
  const listar = (arq) => ler(arq).filter((v) => v.caminho && fs.existsSync(v.caminho)).sort((a, b) => String(b.data).localeCompare(String(a.data)));
158
158
  function abrirNoSistema(alvo) {
159
+ if (process.env.PRIMOCODE_SEM_NAVEGADOR === '1') return false; // app do Primo (voz): navegador só com clique explícito
159
160
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
160
161
  const args = process.platform === 'win32' ? ['/c', 'start', '""', alvo] : [alvo];
161
162
  return spawnSync(cmd, args, { stdio: 'ignore' }).status === 0;
package/lib/mcp.js CHANGED
@@ -204,6 +204,7 @@ function conectarHttp(def, { tetoMs = 60000, nome, buscar = globalThis.fetch } =
204
204
  const b64url = (b) => Buffer.from(b).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
205
205
 
206
206
  function abrirUrl(url) {
207
+ if (process.env.PRIMOCODE_SEM_NAVEGADOR === '1') return false; // app do Primo (voz): navegador só com clique explícito
207
208
  const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
208
209
  : process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', url]]
209
210
  : ['xdg-open', [url]];
package/lib/preparar.js CHANGED
@@ -70,6 +70,8 @@ function estudioLinhas(dep = {}) {
70
70
  // marca ativa
71
71
  const mk = tenta(() => require('./marca-kit.js').ativa([process.cwd()]), null);
72
72
  poe(null, 'Marca ativa', mk ? (mk.nome || mk.slug || 'sim') : 'nenhuma — primocode marca criar <nome>');
73
+ // Primo (modo voz)
74
+ tenta(() => linhas.push(...require('./voz.js').linhasDoutor()), null);
73
75
  // MCP
74
76
  const mcp = tenta(() => Object.keys(require('./mcp.js').servidores()), []);
75
77
  poe(null, 'MCP', mcp.length ? mcp.join(', ') : 'nenhum servidor');
package/lib/tools.js CHANGED
@@ -304,6 +304,7 @@ async function runShell(command, timeout = 60000, cwd, env) {
304
304
  // UM argumento, não texto que um shell reinterpreta — sem isso, um valor tipo
305
305
  // `https://x/"; touch /tmp/pwned; echo "` injetava comando (achado de segurança).
306
306
  function openInBrowser(target) {
307
+ if (process.env.PRIMOCODE_SEM_NAVEGADOR === '1') return Promise.resolve({ ok: false, error: 'o navegador está bloqueado no modo voz: só abre quando a pessoa clica' }); // app do Primo (voz): navegador só com clique explícito
307
308
  const p = os.platform();
308
309
  const [cmd, args, opener] = p === 'darwin' ? ['open', [target], 'open']
309
310
  : p === 'win32' ? ['cmd.exe', ['/c', 'start', '', target], 'start']
@@ -0,0 +1,15 @@
1
+ /**
2
+ * voz-ponte.js — o aviso do agente para o modo voz, sem um depender do outro.
3
+ *
4
+ * O agente (lib/act.js) diz "comecei a ferramenta X" aqui; o /voice, se estiver
5
+ * ligado, se inscreve e repassa ao Primo (painel mostra "executando" e ele
6
+ * confirma em voz alta: "rodando os testes"). Sem /voice, ninguém escuta.
7
+ */
8
+ 'use strict';
9
+
10
+ let ouvinte = null;
11
+
12
+ module.exports = {
13
+ escutar: (fn) => { ouvinte = typeof fn === 'function' ? fn : null; },
14
+ ferramenta: (nome, args) => { if (ouvinte) { try { ouvinte(String(nome || ''), args || {}); } catch { /* o aviso nunca derruba o agente */ } } },
15
+ };
package/lib/voz.js CHANGED
@@ -63,8 +63,19 @@ function paraFalar(texto, { frases = 3, teto = 420 } = {}) {
63
63
  return (fala || t.slice(0, teto)).trim();
64
64
  }
65
65
 
66
- /** Onde está o Python 3. Mesmo problema do Estúdio, mesma cura. */
66
+ /* O VENV DO PRIMO. As dependências opcionais (VAD, painel em tela cheia) moram
67
+ * em ~/.primocode/voz-venv, instaladas sob demanda: ninguém precisa quebrar o
68
+ * Python do sistema (PEP 668) nem perde nada se não instalar. */
69
+ const VENV = path.join(os.homedir(), '.primocode', 'voz-venv');
70
+ const venvPython = () => path.join(VENV, process.platform === 'win32' ? 'Scripts' : 'bin', process.platform === 'win32' ? 'python.exe' : 'python');
71
+
72
+ /** Onde está o Python 3. O do venv do Primo ganha, se já existir. */
67
73
  function python() {
74
+ if (fs.existsSync(venvPython())) return venvPython();
75
+ return pythonDoSistema();
76
+ }
77
+
78
+ function pythonDoSistema() {
68
79
  const nomes = process.platform === 'win32'
69
80
  ? ['python', 'py', 'python3']
70
81
  : ['python3', 'python', '/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3'];
@@ -223,7 +234,7 @@ function instalar(py, pacotes, aoFalar = () => {}) {
223
234
  * @param {()=>void} [op.aoPronto] o microfone abriu
224
235
  * @returns {{processo, pronto: Promise, falar:(t:string)=>void, calar:()=>void}}
225
236
  */
226
- function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {}, aoPronto = () => {} }) {
237
+ function abrir({ python: py, servidor, token, voz, aoParar = () => {}, aoOuvir, mostrar = () => {}, aoPronto = () => {} }) {
227
238
  let p;
228
239
  const manda = (obj) => {
229
240
  try { p.stdin.write(JSON.stringify(obj) + '\n'); } catch { /* já fechou */ }
@@ -237,6 +248,7 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
237
248
  PRIMOCODE_SERVER: servidor,
238
249
  PRIMOCODE_TOKEN: token || '',
239
250
  PRIMOCODE_VOZ: voz || 'antonio',
251
+ PRIMOCODE_CWD: process.cwd(),
240
252
  PYTHONUNBUFFERED: '1',
241
253
  // O Whisper deixa um semáforo para o sistema recolher na saída,
242
254
  // e o Python avisa disso no stderr. É inofensivo; o aviso não é.
@@ -254,6 +266,7 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
254
266
  try { msg = JSON.parse(linha); } catch { continue; }
255
267
  if (msg.tipo === 'fim') { try { p.kill(); } catch {} return; }
256
268
  if (msg.tipo === 'pronto') { try { aoPronto(); } catch {} continue; }
269
+ if (msg.tipo === 'parar') { try { aoParar(); } catch {} continue; }
257
270
  if (msg.tipo === 'erro') { mostrar(String(msg.texto || ''), true); continue; }
258
271
  if (msg.tipo === 'ouvi' && String(msg.texto || '').trim()) {
259
272
  try { aoOuvir(String(msg.texto).trim()); } catch (e) { mostrar(e.message, true); }
@@ -276,7 +289,12 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
276
289
  return {
277
290
  processo: p,
278
291
  pronto,
279
- falar: (texto) => { const t = paraFalar(texto); if (t) manda({ tipo: 'falar', texto: t }); },
292
+ /* O texto vai BRUTO: quem resume é o Primo (voz/primo_voz/conversa.py) —
293
+ a voz diz o resumo, o painel mostra o resto (código, listas). */
294
+ falar: (texto) => { const t = String(texto || '').trim(); if (t) manda({ tipo: 'falar', texto: t, bruto: true }); },
295
+ // O agente começou uma ferramenta: o painel mostra "executando" e o Primo confirma ("rodando os testes").
296
+ ferramenta: (nome, args) => manda({ tipo: 'ferramenta', nome, args: args || {} }),
297
+ painel: (acao) => manda({ tipo: 'painel', acao }),
280
298
  calar: () => manda({ tipo: 'calar' }),
281
299
  // A bolinha na tela: 'esperando' quando o terminal termina sem falar.
282
300
  estado: (e) => manda({ tipo: 'estado', estado: e }),
@@ -303,7 +321,11 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
303
321
  */
304
322
  const OPCIONAIS = () => [
305
323
  ['faster_whisper', 'faster-whisper', 'a transcrição local (Whisper)'],
324
+ ['pysilero_vad', 'pysilero-vad', 'o fim de fala preciso (Silero VAD)'],
325
+ ['edge_tts', 'edge-tts', 'a voz neural pt-BR (Edge TTS)'],
306
326
  ...(process.platform === 'darwin' ? [
327
+ ['WebKit', 'pyobjc-framework-WebKit', 'o painel em tela cheia do Primo'],
328
+ ['EventKit', 'pyobjc-framework-EventKit', 'os compromissos de hoje do app Calendário'],
307
329
  ['Quartz', 'pyobjc-framework-Quartz', 'o cursor do Primo na tela'],
308
330
  ['Vision', 'pyobjc-framework-Vision', 'ler o texto da tela (clicar pelo nome em qualquer app)'],
309
331
  ] : []),
@@ -394,13 +416,23 @@ async function baixarWhisper(py) {
394
416
  * @returns {Promise<{ok:boolean, python?:string, esperando?:boolean,
395
417
  * avisos:string[], error?:string, comoResolver?:string}>}
396
418
  */
419
+ async function criarVenv(aoFalar) {
420
+ if (fs.existsSync(venvPython())) return null;
421
+ const base = pythonDoSistema();
422
+ if (!base) return null;
423
+ aoFalar('criando o ambiente do Primo (~/.primocode/voz-venv)');
424
+ const r = await rodar(base, ['-m', 'venv', VENV], 5 * 60 * 1000);
425
+ return r.ok ? null : `não consegui criar o ambiente do Primo (${porqueOPipFalhou(r.saida)}) — uso o Python do sistema.`;
426
+ }
427
+
397
428
  async function preparar(aoFalar = () => {}) {
429
+ const avisoVenv = await criarVenv(aoFalar);
398
430
  const py = python();
399
431
  if (!py) {
400
432
  return { ok: false, avisos: [], error: 'o modo voz precisa do Python 3, e não achei nenhum nesta máquina.',
401
433
  comoResolver: process.platform === 'darwin' ? 'brew install python' : 'instale em python.org e abra o terminal de novo' };
402
434
  }
403
- const avisos = [];
435
+ const avisos = avisoVenv ? [avisoVenv] : [];
404
436
 
405
437
  // 1. sistema, depois 2. os obrigatórios pelo pip.
406
438
  let falta = faltando(py);
@@ -435,4 +467,66 @@ async function preparar(aoFalar = () => {}) {
435
467
  return { ok: true, python: py, avisos };
436
468
  }
437
469
 
438
- module.exports = { DICA_COMPUTADOR, paraFalar, conferir, instalar, preparar, porqueOPipFalhou, abrir, python, PASTA };
470
+ /** Primo.app em ~/Applications (Launchpad/Spotlight) + o primo-app.json com os caminhos ABSOLUTOS de node e primocode:
471
+ * o app aberto pelo Finder não tem o PATH do terminal. Chamado no primeiro /voice e por `primocode voz instalar-app`. */
472
+ function instalarApp({ python: py, dock = false } = {}) {
473
+ if (process.platform !== 'darwin') return { ok: false, erro: 'o app do Primo é só para macOS' };
474
+ try {
475
+ let cfg = {};
476
+ try { cfg = require('./config').loadConfig(); } catch { /* usa o padrão do app */ }
477
+ const arq = path.join(os.homedir(), '.primocode', 'primo-app.json');
478
+ fs.mkdirSync(path.dirname(arq), { recursive: true });
479
+ fs.writeFileSync(arq, JSON.stringify({
480
+ node: process.execPath, primocode: path.resolve(__dirname, '..', 'bin', 'primocode.js'),
481
+ servidor: cfg.server || '', path: process.env.PATH || '', voz: cfg.voz || 'antonio', versao: require('../package.json').version,
482
+ }, null, 2));
483
+ const env = { ...process.env, PYTHONPATH: PASTA, ...(dock ? { PRIMOCODE_APP_DOCK: '1' } : {}) };
484
+ const linhas = execFileSync(py, ['-m', 'primo_voz.appmac'], { cwd: PASTA, env, encoding: 'utf8', timeout: 60000, stdio: ['ignore', 'pipe', 'ignore'] }).trim().split('\n');
485
+ return { ok: !!linhas[0], exe: linhas[0] || null, pythonpath: linhas[1] || PASTA, app: path.join(os.homedir(), 'Applications', 'Primo.app') };
486
+ } catch (e) { return { ok: false, erro: e.message }; }
487
+ }
488
+
489
+ /** `/voice painel`: só o HUD (sem microfone). Mac com PyObjC = tela cheia; senão, o quadro no terminal. */
490
+ function abrirPainel({ python: py }) {
491
+ const mac = process.platform === 'darwin' && temModulo(py, 'WebKit');
492
+ // No Mac o painel roda pelo Primo.app (Dock e Cmd+Tab mostram "Primo" com o ícone); sem ele, o python direto.
493
+ let exe = null, pp = PASTA;
494
+ if (mac) { const r = instalarApp({ python: py }); if (r.exe) { exe = r.exe; pp = r.pythonpath; } }
495
+ const [cmd, args] = exe ? [exe, ['-m', 'primo_voz.hud', '--livre']] : [py, ['-m', mac ? 'primo_voz.hud' : 'primo_voz.painel_tui', ...(mac ? ['--livre'] : [])]];
496
+ const p = spawn(cmd, args, {
497
+ cwd: PASTA, env: { ...process.env, PYTHONPATH: exe ? pp : PASTA }, stdio: mac ? 'ignore' : ['pipe', 'inherit', 'inherit'],
498
+ });
499
+ return { processo: p, tela: mac };
500
+ }
501
+
502
+ /** Grava o nome do assistente em ~/.primocode/voz.json (o mesmo arquivo que voz/primo_voz/identidade.py lê). */
503
+ function definirNome(nome) {
504
+ const limpo = String(nome || '').replace(/[^\p{L}\p{N} -]/gu, '').trim();
505
+ if (!limpo || limpo.split(/\s+/).length > 2 || limpo.length < 2 || limpo.length > 20) return null;
506
+ const arq = process.env.PRIMOCODE_VOZ_CONFIG || path.join(os.homedir(), '.primocode', 'voz.json');
507
+ let cfg = {};
508
+ try { cfg = JSON.parse(fs.readFileSync(arq, 'utf8')); } catch { /* primeira vez */ }
509
+ cfg.nome = limpo.split(/\s+/).map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
510
+ fs.mkdirSync(path.dirname(arq), { recursive: true });
511
+ fs.writeFileSync(arq, JSON.stringify(cfg, null, 2));
512
+ return cfg.nome;
513
+ }
514
+
515
+ /** As linhas do `primocode doutor` sobre o modo voz. */
516
+ function linhasDoutor() {
517
+ const py = python();
518
+ if (!py) return [{ ok: false, item: 'Primo (voz)', detalhe: 'sem Python 3', conserto: 'brew install python' }];
519
+ const tem = (m) => temModulo(py, m);
520
+ const venv = fs.existsSync(venvPython());
521
+ const linha = (ok, item, detalhe, conserto) => ({ ok, item, detalhe, conserto: ok === false ? conserto : undefined });
522
+ return [
523
+ linha(venv ? true : null, 'Primo: ambiente', venv ? VENV : 'ainda não criado — /voice cria sozinho', undefined),
524
+ linha(tem('sounddevice') && tem('numpy'), 'Primo: microfone', 'sounddevice + numpy', '/voice instala; ou: ' + py + ' -m pip install sounddevice numpy'),
525
+ linha(tem('faster_whisper') ? true : null, 'Primo: Whisper local', tem('faster_whisper') ? 'instalado' : 'não instalado — usa o servidor'),
526
+ linha(tem('pysilero_vad') ? true : null, 'Primo: Silero VAD', tem('pysilero_vad') ? 'fim de fala preciso' : 'não instalado — usa o corte por volume'),
527
+ linha(process.platform === 'darwin' ? (tem('WebKit') ? true : null) : null, 'Primo: tela cheia',
528
+ process.platform !== 'darwin' ? 'só no Mac — aqui, painel no terminal' : tem('WebKit') ? 'pronto' : 'sem PyObjC WebKit — usa a orbe'),
529
+ ];
530
+ }
531
+
532
+ module.exports = { instalarApp, definirNome, VENV, abrirPainel, linhasDoutor, DICA_COMPUTADOR, paraFalar, conferir, instalar, preparar, porqueOPipFalhou, abrir, python, PASTA };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.14.1",
3
+ "version": "10.15.1",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "start": "node bin/primocode.js",
11
11
  "postinstall": "node lib/preparar.js || true",
12
12
  "test:server": "cd server && npm test",
13
- "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js"
13
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/primo-voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js"
14
14
  },
15
15
  "engines": {
16
16
  "node": ">=18.17.0"
@@ -32,6 +32,7 @@
32
32
  "studio/",
33
33
  "skills/",
34
34
  "voz/",
35
+ "LICENSES/",
35
36
  "README.md",
36
37
  "comandos/",
37
38
  "!**/__pycache__",