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.
- package/LICENSES/robo.txt +19 -0
- package/README.md +6 -2
- package/bin/primocode.js +54 -3
- package/lib/act.js +1 -0
- package/lib/conta.js +1 -0
- package/lib/estudio/exportar.js +1 -0
- package/lib/mcp.js +1 -0
- package/lib/preparar.js +2 -0
- package/lib/tools.js +1 -0
- package/lib/voz-ponte.js +15 -0
- package/lib/voz.js +99 -5
- package/package.json +3 -2
- package/voz/primo_voz/__main__.py +429 -57
- package/voz/primo_voz/app.py +350 -0
- package/voz/primo_voz/appmac.py +124 -0
- package/voz/primo_voz/assets/mascote.svg +1 -0
- package/voz/primo_voz/conversa.py +236 -0
- package/voz/primo_voz/escuta.py +56 -4
- package/voz/primo_voz/fala.py +87 -0
- package/voz/primo_voz/hud.html +584 -0
- package/voz/primo_voz/hud.py +275 -0
- package/voz/primo_voz/identidade.py +88 -0
- package/voz/primo_voz/luz.py +137 -0
- package/voz/primo_voz/painel.py +459 -0
- package/voz/primo_voz/painel_tui.py +26 -0
- package/voz/primo_voz/sons.py +96 -0
- package/voz/primo_voz/tela.py +319 -0
- package/voz/primo_voz/transcrever.py +13 -2
- package/voz/primo_voz/vendor/RobotExpressive.glb +0 -0
- package/voz/primo_voz/vendor/primo-icone.png +0 -0
- package/voz/primo_voz/vendor/spline-runtime.min.js +8219 -0
- package/voz/primo_voz/vendor/three-robo.min.js +3824 -0
- package/voz/primo_voz/vendor/three.LICENSE.txt +21 -0
- package/voz/primo_voz/vivo.py +34 -0
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
2478
|
-
|
|
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]];
|
package/lib/estudio/exportar.js
CHANGED
|
@@ -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']
|
package/lib/voz-ponte.js
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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__",
|