primocode 9.0.0 → 9.2.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.
- package/README.md +11 -17
- package/bin/primocode.js +88 -15
- package/lib/act.js +2 -2
- package/lib/boas-vindas.js +13 -5
- package/lib/catalogo.js +40 -1
- package/lib/entrada.js +40 -9
- package/lib/{acervo.js → estudio/acervo.js} +1 -1
- package/lib/estudio/assets.js +1 -1
- package/lib/estudio/identidade.js +2 -2
- package/lib/estudio/index.js +1 -1
- package/lib/{marca.js → estudio/marca.js} +1 -1
- package/lib/estudio/remotion.js +40 -2
- package/lib/fala.js +36 -5
- package/lib/markdown.js +376 -0
- package/lib/pasta.js +15 -4
- package/lib/tools.js +55 -6
- package/lib/voz.js +104 -4
- package/package.json +2 -2
- package/studio/pessoa.py +33 -1
- package/voz/primo_voz/__main__.py +138 -16
- package/voz/primo_voz/desperta.py +195 -0
- package/voz/primo_voz/escuta.py +5 -4
- package/voz/primo_voz/orbe.py +42 -0
- /package/lib/{diretor.js → estudio/diretor.js} +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.
|
|
1
|
+
# PrimoCode v9.2.0
|
|
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 v9.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.2.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -33,25 +33,19 @@ Em uma frase: **é o poder de um agente de código no terminal, em português, c
|
|
|
33
33
|
|
|
34
34
|
## Instalação
|
|
35
35
|
|
|
36
|
-
**macOS / Linux / WSL** — um comando, e ele resolve tudo (inclusive instalar o Node, se faltar):
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
curl -fsSL https://raw.githubusercontent.com/ConectaPrimoAI/primocode/main/install.sh | bash
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Windows** — baixe o repositório e rode:
|
|
43
|
-
|
|
44
|
-
```cmd
|
|
45
|
-
install.cmd
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Já tem Node 18.17+? Então basta:
|
|
49
|
-
|
|
50
36
|
```bash
|
|
51
37
|
npm install -g primocode
|
|
52
38
|
```
|
|
53
39
|
|
|
54
|
-
Requer **Node.js 18.17+**. O CLI não tem
|
|
40
|
+
Requer **Node.js 18.17+**. O CLI não tem dependência nenhuma — só a stdlib do
|
|
41
|
+
Node — e por isso instala em segundos. Depois é só `primocode`: ele abre o
|
|
42
|
+
login no navegador e, voltando, está pronto.
|
|
43
|
+
|
|
44
|
+
Não tem Node? Instale em [nodejs.org](https://nodejs.org) e repita o comando
|
|
45
|
+
acima. Quem já clonou este repositório pode usar os instaladores que moram
|
|
46
|
+
aqui (`install.sh` no macOS/Linux/WSL, `install.cmd` no Windows): eles detectam
|
|
47
|
+
o Node, oferecem instalar, e apontam o npm para `~/.npm-global` em vez da pasta
|
|
48
|
+
do sistema — que é o que evita o `EACCES` da tabela abaixo.
|
|
55
49
|
|
|
56
50
|
### Se der erro
|
|
57
51
|
|
package/bin/primocode.js
CHANGED
|
@@ -151,7 +151,8 @@ const SLASH_COMANDOS = [
|
|
|
151
151
|
['/account', 'sua conta, plano e computadores conectados'],
|
|
152
152
|
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
153
153
|
['/logout', 'desconecta este computador da conta'],
|
|
154
|
-
['/voice', '
|
|
154
|
+
['/voice', 'fica ouvindo em segundo plano — diga "ei, primo" e a orb aparece'],
|
|
155
|
+
['/voice parar', 'desliga o modo voz e fecha o microfone'],
|
|
155
156
|
['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
|
|
156
157
|
['/marketplace', 'a tela das skills: navegue, ligue, crie'],
|
|
157
158
|
['/install-skill', 'instala uma skill de um arquivo'],
|
|
@@ -966,6 +967,11 @@ async function comprimirConversa(forcado = false) {
|
|
|
966
967
|
// na tela depois de o usuário ter mandado parar.
|
|
967
968
|
let chatEmVoo = null;
|
|
968
969
|
|
|
970
|
+
/* O ouvinte de voz em segundo plano.
|
|
971
|
+
Um por sessão: dois processos disputando o mesmo microfone dão um vazio
|
|
972
|
+
intermitente que parece defeito de hardware e não é. */
|
|
973
|
+
let vozEmEspera = null;
|
|
974
|
+
|
|
969
975
|
async function runChat(promptForModel, userTextForMemory) {
|
|
970
976
|
chatEmVoo = new AbortController();
|
|
971
977
|
const sp = new Spinner(ui.PENSANDO).start();
|
|
@@ -1405,6 +1411,24 @@ async function handleSlash(text) {
|
|
|
1405
1411
|
* garantia de que um deles está errado.
|
|
1406
1412
|
*/
|
|
1407
1413
|
case 'voice': case 'voz': {
|
|
1414
|
+
/* ── /voice PARA ─────────────────────────────────────────────
|
|
1415
|
+
Encerrar é o primeiro caso tratado de propósito: quem quer
|
|
1416
|
+
desligar o microfone quer desligar AGORA, e não deve esbarrar
|
|
1417
|
+
em checagem de dependência nem em download de modelo. */
|
|
1418
|
+
if (/^(parar|stop|off|sair)$/i.test((rest || '').trim())) {
|
|
1419
|
+
if (!vozEmEspera) { console.log(c.muted(' O modo voz não está ligado.')); break; }
|
|
1420
|
+
try { vozEmEspera.kill(); } catch {}
|
|
1421
|
+
vozEmEspera = null;
|
|
1422
|
+
console.log(c.muted(' Modo voz desligado. O microfone fechou.'));
|
|
1423
|
+
break;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (vozEmEspera) {
|
|
1427
|
+
console.log(c.muted(' O modo voz já está ouvindo. Diga "ei, primo".'));
|
|
1428
|
+
console.log(c.dim(' /voice parar desliga.'));
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1408
1432
|
const pronto = vozModo.conferir();
|
|
1409
1433
|
if (!pronto.ok && !pronto.instalavel) {
|
|
1410
1434
|
console.log(c.err(` ${pronto.error}`));
|
|
@@ -1430,29 +1454,59 @@ async function handleSlash(text) {
|
|
|
1430
1454
|
const conferido = vozModo.conferir();
|
|
1431
1455
|
if (!conferido.ok) {
|
|
1432
1456
|
console.log(c.err(` ainda falta: ${conferido.error}`));
|
|
1433
|
-
if (conferido.comoResolver) console.log(c.muted(` ${conferido.comoResolver}`));
|
|
1434
1457
|
break;
|
|
1435
1458
|
}
|
|
1436
1459
|
}
|
|
1437
1460
|
|
|
1438
1461
|
const py = (pronto.ok ? pronto : vozModo.conferir()).python;
|
|
1462
|
+
|
|
1463
|
+
/* ── O RECONHECEDOR LOCAL ────────────────────────────────────
|
|
1464
|
+
Ele é o que permite o microfone ficar aberto sem o áudio sair
|
|
1465
|
+
da máquina: o nome é reconhecido AQUI, e só depois disso a
|
|
1466
|
+
frase viaja. São ~31 MB, uma vez, e ficam em ~/.primocode.
|
|
1467
|
+
|
|
1468
|
+
Se a pessoa recusar, o modo voz ainda abre — mas já ouvindo,
|
|
1469
|
+
sem espera. O que NÃO acontece em hipótese nenhuma é cair para
|
|
1470
|
+
"manda tudo para o servidor o dia inteiro": isso seria trocar
|
|
1471
|
+
a privacidade dela por conveniência sem perguntar. */
|
|
1472
|
+
const espera = await vozModo.conferirEspera(py);
|
|
1473
|
+
let esperando = espera.ok;
|
|
1474
|
+
if (!espera.ok && espera.baixavel) {
|
|
1475
|
+
console.log('');
|
|
1476
|
+
console.log(' ' + c.muted('Para ficar ouvindo só o nome, sem mandar áudio para lugar nenhum,'));
|
|
1477
|
+
console.log(' ' + c.muted('ele precisa do reconhecedor local — ') + c.dim('~31 MB, uma vez.'));
|
|
1478
|
+
if (await confirm('Baixar agora?')) {
|
|
1479
|
+
const sp = new Spinner('baixando o reconhecedor').start();
|
|
1480
|
+
const r = await vozModo.baixarEspera(py);
|
|
1481
|
+
sp.stop();
|
|
1482
|
+
if (r.ok) esperando = true;
|
|
1483
|
+
else {
|
|
1484
|
+
console.log(c.err(` não deu: ${r.error}`));
|
|
1485
|
+
if (r.comoResolver) console.log(c.dim(` ${r.comoResolver}`));
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
} else if (!espera.ok && espera.error) {
|
|
1489
|
+
console.log(c.dim(` ${espera.error}`));
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1439
1492
|
console.log('');
|
|
1440
1493
|
console.log(' ' + c.bold(c.brand('Primo Code Voice')));
|
|
1441
|
-
|
|
1442
|
-
+ c.
|
|
1443
|
-
|
|
1494
|
+
if (esperando) {
|
|
1495
|
+
console.log(' ' + c.muted('Ouvindo em segundo plano. Diga ') + c.white('"ei, primo"') + c.muted(' e a orb aparece.'));
|
|
1496
|
+
console.log(' ' + c.dim('Nada sai da máquina até você chamar · /voice parar desliga'));
|
|
1497
|
+
} else {
|
|
1498
|
+
console.log(' ' + c.muted('É só falar. A orb responde: ')
|
|
1499
|
+
+ c.dim('parada espera · pulsa ouvindo · respira pensando · brilha falando'));
|
|
1500
|
+
console.log(' ' + c.dim('Falar por cima corta a resposta · /voice parar desliga'));
|
|
1501
|
+
}
|
|
1444
1502
|
console.log('');
|
|
1445
1503
|
|
|
1446
|
-
|
|
1447
|
-
// está digitando, e uma caixa piscando embaixo é ruído.
|
|
1448
|
-
if (rl && rl.suspender) rl.suspender();
|
|
1449
|
-
sliderAtivo = true;
|
|
1450
|
-
|
|
1451
|
-
const r = await vozModo.abrir({
|
|
1504
|
+
const sessao = vozModo.abrir({
|
|
1452
1505
|
python: py,
|
|
1453
1506
|
servidor: state.config.server,
|
|
1454
1507
|
token: state.config.token,
|
|
1455
1508
|
voz: state.config.voz || 'antonio',
|
|
1509
|
+
esperando,
|
|
1456
1510
|
mostrar: (texto, deErro) => {
|
|
1457
1511
|
if (deErro) console.log(' ' + c.dim(texto));
|
|
1458
1512
|
else console.log(' ' + c.brand('❯ ') + c.white(texto));
|
|
@@ -1485,10 +1539,17 @@ async function handleSlash(text) {
|
|
|
1485
1539
|
},
|
|
1486
1540
|
});
|
|
1487
1541
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1542
|
+
/* O TERMINAL CONTINUA SEU.
|
|
1543
|
+
Aqui NÃO se espera o fim. Era isso que obrigava a digitar
|
|
1544
|
+
/voice antes de cada pedido: o comando bloqueava o prompt, e a
|
|
1545
|
+
única forma de voltar a digitar era encerrar o modo voz. Agora
|
|
1546
|
+
ele vive ao lado — a pessoa fala quando quiser e digita quando
|
|
1547
|
+
quiser, sem desligar um para usar o outro. */
|
|
1548
|
+
vozEmEspera = sessao.processo;
|
|
1549
|
+
sessao.pronto.then((r) => {
|
|
1550
|
+
vozEmEspera = null;
|
|
1551
|
+
if (r && !r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
|
|
1552
|
+
});
|
|
1492
1553
|
break;
|
|
1493
1554
|
}
|
|
1494
1555
|
|
|
@@ -1971,6 +2032,18 @@ async function main() {
|
|
|
1971
2032
|
function sayBye() {
|
|
1972
2033
|
if (saidBye) return;
|
|
1973
2034
|
saidBye = true;
|
|
2035
|
+
|
|
2036
|
+
/* O MICROFONE FECHA JUNTO.
|
|
2037
|
+
O ouvinte de voz é um processo filho, e filho no POSIX não morre
|
|
2038
|
+
porque o pai saiu: ficaria um Python segurando o microfone depois
|
|
2039
|
+
de o terminal fechar — sem janela, sem jeito de desligar a não ser
|
|
2040
|
+
pelo gerenciador de tarefas, e com a luzinha da câmera do lado
|
|
2041
|
+
acesa. Matar aqui é o que faz "fechei o terminal" significar
|
|
2042
|
+
"fechei tudo". */
|
|
2043
|
+
if (vozEmEspera) {
|
|
2044
|
+
try { vozEmEspera.kill(); } catch { /* já morreu */ }
|
|
2045
|
+
vozEmEspera = null;
|
|
2046
|
+
}
|
|
1974
2047
|
// O último turno pode ainda estar subindo — gravar é de propósito
|
|
1975
2048
|
// solto, para não segurar o prompt. Fechar o terminal em cima dele
|
|
1976
2049
|
// perderia justamente a resposta que a pessoa acabou de ler, e ela
|
package/lib/act.js
CHANGED
|
@@ -539,7 +539,7 @@ async function runAct(userPrompt, opts) {
|
|
|
539
539
|
* Num subagente não entra: ele já está fazendo uma parte de um plano, e
|
|
540
540
|
* um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
|
|
541
541
|
if (!subagent && profundidade === 0) {
|
|
542
|
-
const producao = require('./diretor.js').precisaDeProducao(pedido);
|
|
542
|
+
const producao = require('./estudio/diretor.js').precisaDeProducao(pedido);
|
|
543
543
|
if (producao.producao) {
|
|
544
544
|
messages.push({ role: 'system', content:
|
|
545
545
|
'ESTE PEDIDO É DE PRODUÇÃO AUDIOVISUAL. Antes de criar qualquer peça, chame '
|
|
@@ -1108,7 +1108,7 @@ async function rodarDiretor(args, ctx) {
|
|
|
1108
1108
|
return { ok: false, error: 'Recusado: o diretor não roda dentro de um subagente. '
|
|
1109
1109
|
+ 'Produza com o plano que você já recebeu.' };
|
|
1110
1110
|
}
|
|
1111
|
-
const diretor = require('./diretor.js');
|
|
1111
|
+
const diretor = require('./estudio/diretor.js');
|
|
1112
1112
|
const correr = async (prompt, rotulo) => {
|
|
1113
1113
|
if (PARAR) return '';
|
|
1114
1114
|
fala.agente('Diretor', rotulo, profundidade);
|
package/lib/boas-vindas.js
CHANGED
|
@@ -122,8 +122,12 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
122
122
|
return { liberado: false, motivo: 'sem-plano', plano };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
/* Nada de "Conta liberada." na abertura.
|
|
126
|
+
Estar logado é o estado NORMAL do produto — anunciar isso toda vez é
|
|
127
|
+
ruído, e ruído no primeiro instante empurra para baixo o que importa,
|
|
128
|
+
que é a caixa de digitar. O selo do plano continua existindo onde quem
|
|
129
|
+
procura vai olhar (/account, /status); a abertura só diz alguma coisa
|
|
130
|
+
quando há algo a resolver. */
|
|
127
131
|
|
|
128
132
|
/* Não há etapa 3. O que vinha aqui era a escolha do provedor de IA, e
|
|
129
133
|
ela morreu junto com o `/claude` e o `/gpt`: a API já vem com o
|
|
@@ -133,9 +137,13 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
133
137
|
const primeira = !jaEscolheu();
|
|
134
138
|
if (primeira) marcar({ plano });
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
/* Na primeira vez a frase serve: a pessoa acabou de instalar e não sabe
|
|
141
|
+
que já pode pedir. Da segunda em diante ela é só mais uma linha entre
|
|
142
|
+
a pessoa e o cursor. */
|
|
143
|
+
if (primeira) {
|
|
144
|
+
mostrar('');
|
|
145
|
+
mostrar(c.muted(' Tudo pronto. Pode pedir a primeira coisa.'));
|
|
146
|
+
}
|
|
139
147
|
mostrar('');
|
|
140
148
|
return { liberado: true, plano, primeira, perguntou: false };
|
|
141
149
|
}
|
package/lib/catalogo.js
CHANGED
|
@@ -206,4 +206,43 @@ function escreveArquivo(tool) {
|
|
|
206
206
|
return ESCREVEM.has(tool);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* As ferramentas de nome mais parecido com o que foi chamado.
|
|
212
|
+
*
|
|
213
|
+
* Serve a um caso só, e ele é real: o modelo chamou uma tool que não existe.
|
|
214
|
+
* Isso acontece quando a lista que o servidor oferece diverge deste catálogo
|
|
215
|
+
* — foi o que houve quando o Estúdio trocou de vocabulário e o servidor
|
|
216
|
+
* continuou oferecendo `studio_criar` e `studio_listar`. Sem esta lista, a
|
|
217
|
+
* resposta ao modelo era "Tool não implementada", ele não tinha como saber o
|
|
218
|
+
* que existe, e dizia ao usuário que a ferramenta "não está disponível".
|
|
219
|
+
*
|
|
220
|
+
* A semelhança é por prefixo e por pedaço de nome, não por distância de
|
|
221
|
+
* edição: os erros reais são de FAMÍLIA ("studio_criar" quando o certo é
|
|
222
|
+
* "estudio_plano"), não de digitação.
|
|
223
|
+
*/
|
|
224
|
+
function parecidasCom(nome) {
|
|
225
|
+
const alvo = String(nome || '').toLowerCase().replace(/^estudio_|^studio_/, '');
|
|
226
|
+
const familia = /^e?studio_/.test(String(nome || '').toLowerCase());
|
|
227
|
+
const pontos = TODAS.map((t) => {
|
|
228
|
+
let p = 0;
|
|
229
|
+
if (familia && /^estudio_|^planejar_/.test(t)) p += 3;
|
|
230
|
+
const semPrefixo = t.replace(/^estudio_/, '');
|
|
231
|
+
if (semPrefixo === alvo) p += 10;
|
|
232
|
+
else if (semPrefixo.includes(alvo) || alvo.includes(semPrefixo)) p += 5;
|
|
233
|
+
// pedaços em comum (criar/listar/plano/midia…)
|
|
234
|
+
for (const parte of alvo.split('_')) {
|
|
235
|
+
if (parte.length >= 4 && t.includes(parte)) p += 2;
|
|
236
|
+
}
|
|
237
|
+
return { t, p };
|
|
238
|
+
}).filter((x) => x.p > 0).sort((a, b) => b.p - a.p);
|
|
239
|
+
// Sem nada parecido, a resposta honesta é o grupo inteiro a que ele
|
|
240
|
+
// provavelmente queria chegar — melhor que uma lista vazia.
|
|
241
|
+
if (!pontos.length) {
|
|
242
|
+
return familia ? TODAS.filter((t) => /^estudio_|^planejar_/.test(t)) : TODAS.slice(0, 8);
|
|
243
|
+
}
|
|
244
|
+
return pontos.slice(0, 6).map((x) => x.t);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
parecidasCom, GRUPOS, TODAS, ESCREVEM, escreveArquivo };
|
package/lib/entrada.js
CHANGED
|
@@ -93,7 +93,7 @@ function montar(e) {
|
|
|
93
93
|
const colunas = Math.max(24, Math.min(e.colunas || 80, 200));
|
|
94
94
|
const largura = colunas - 1; // a última coluna fica livre: escrever nela
|
|
95
95
|
// faz o terminal quebrar a linha sozinho
|
|
96
|
-
const w = Math.max(8, largura -
|
|
96
|
+
const w = Math.max(8, largura - 3); // espaço + '❯ ' — e mais nada à direita
|
|
97
97
|
|
|
98
98
|
const cursor = Math.max(0, Math.min(e.cursor | 0, String(e.texto).length));
|
|
99
99
|
const linhas = quebrar(e.texto, w);
|
|
@@ -108,9 +108,16 @@ function montar(e) {
|
|
|
108
108
|
if (cursor >= l.de && cursor <= fim) { cursorLinha = i; cursorColuna = cursor - l.de; }
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/* DUAS RÉGUAS, NÃO UMA CAIXA.
|
|
112
|
+
Era uma caixa fechada — cantos e barras laterais. O Claude Code, que é a
|
|
113
|
+
referência pedida, usa só a linha de cima e a de baixo, e a diferença
|
|
114
|
+
não é gosto: a barra lateral direita obriga a reservar duas colunas em
|
|
115
|
+
toda linha e a preencher o resto com espaço, e é ela que faz o texto
|
|
116
|
+
colado parecer desalinhado quando o terminal é estreito. Sem as laterais
|
|
117
|
+
o campo ganha as colunas de volta e a emenda some. */
|
|
111
118
|
const out = [];
|
|
112
|
-
const regua = c.dim(BORDA.h.repeat(largura
|
|
113
|
-
out.push(
|
|
119
|
+
const regua = c.dim(BORDA.h.repeat(largura));
|
|
120
|
+
out.push(regua);
|
|
114
121
|
|
|
115
122
|
const vazio = !String(e.texto).length;
|
|
116
123
|
linhas.forEach((l, i) => {
|
|
@@ -124,10 +131,12 @@ function montar(e) {
|
|
|
124
131
|
conteudo += c.dim(e.fantasma);
|
|
125
132
|
}
|
|
126
133
|
if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
|
|
127
|
-
|
|
134
|
+
// Sem `encher`: sem barra à direita não há nada para alinhar, e o
|
|
135
|
+
// espaço sobrando só apagaria o que estivesse embaixo.
|
|
136
|
+
out.push(' ' + marca + conteudo);
|
|
128
137
|
});
|
|
129
138
|
|
|
130
|
-
out.push(
|
|
139
|
+
out.push(regua);
|
|
131
140
|
|
|
132
141
|
// ── o menu dos comandos ──────────────────────────────────────────────
|
|
133
142
|
// Fica ABAIXO da caixa, não dentro: dentro, ele empurraria o campo de
|
|
@@ -157,8 +166,8 @@ function montar(e) {
|
|
|
157
166
|
|
|
158
167
|
return {
|
|
159
168
|
linhas: out,
|
|
160
|
-
cursorLinha: 1 + cursorLinha, // +1 pela
|
|
161
|
-
cursorColuna: 1 +
|
|
169
|
+
cursorLinha: 1 + cursorLinha, // +1 pela régua de cima
|
|
170
|
+
cursorColuna: 1 + 2 + cursorColuna, // espaço + '❯ '
|
|
162
171
|
};
|
|
163
172
|
}
|
|
164
173
|
|
|
@@ -221,10 +230,27 @@ class Entrada extends EventEmitter {
|
|
|
221
230
|
this._stdoutOriginal = process.stdout.write;
|
|
222
231
|
const eu = this;
|
|
223
232
|
process.stdout.write = function (texto, ...resto) {
|
|
224
|
-
if (eu._pintando
|
|
233
|
+
if (eu._pintando) return eu._stdoutOriginal.call(this, texto, ...resto);
|
|
225
234
|
eu._apagar();
|
|
226
235
|
const r = eu._stdoutOriginal.call(this, texto, ...resto);
|
|
227
|
-
|
|
236
|
+
/* ── O TEXTO QUE CHEGA EM PEDAÇOS ────────────────────────────
|
|
237
|
+
A resposta do chat sai em fluxo: dezenas de `write` pequenos,
|
|
238
|
+
nenhum deles terminando em quebra de linha. Cada um deixa o
|
|
239
|
+
cursor NO MEIO da linha — e o desenho da caixa começa com
|
|
240
|
+
"\r\x1b[0J", que volta ao início da linha e limpa dali para
|
|
241
|
+
baixo. Ou seja: redesenhar ali apagava o pedaço que acabara de
|
|
242
|
+
ser escrito, e o seguinte apagava o próximo.
|
|
243
|
+
|
|
244
|
+
O efeito para quem usa era o pior possível: no modo chat a
|
|
245
|
+
mensagem ia, o modelo respondia, e a tela não mostrava NADA —
|
|
246
|
+
parecia que o programa tinha ignorado a pessoa.
|
|
247
|
+
|
|
248
|
+
Então a caixa espera a linha fechar. Enquanto o cursor estiver
|
|
249
|
+
no meio de uma, ela não é redesenhada; quando a quebra de linha
|
|
250
|
+
chega (o `fechar()` do fluxo manda uma), ela volta. */
|
|
251
|
+
const t = String(texto == null ? '' : texto);
|
|
252
|
+
eu._meiaLinha = t.length > 0 && !t.endsWith('\n');
|
|
253
|
+
if (!eu._meiaLinha) eu._desenhar();
|
|
228
254
|
return r;
|
|
229
255
|
};
|
|
230
256
|
return this;
|
|
@@ -311,6 +337,11 @@ class Entrada extends EventEmitter {
|
|
|
311
337
|
if (!this.tty || this.suspenso || this.fechado) return;
|
|
312
338
|
const q = this._quadro();
|
|
313
339
|
this._pintando = true;
|
|
340
|
+
/* Cursor no meio de uma linha (fluxo que ainda não fechou): desce uma
|
|
341
|
+
antes de desenhar. Vale para quem chama `_desenhar` por fora do
|
|
342
|
+
embrulho do stdout — `prompt()`, o fim de uma tela suspensa — e é o
|
|
343
|
+
que garante que a caixa nunca começa em cima de texto pela metade. */
|
|
344
|
+
if (this._meiaLinha) { this.escreverCru('\n'); this._meiaLinha = false; }
|
|
314
345
|
// `\x1b[0J` limpa daqui até o fim da tela: sem isso, um quadro mais
|
|
315
346
|
// curto que o anterior (menu que fechou) deixa lixo embaixo.
|
|
316
347
|
this.escreverCru('\r\x1b[0J' + q.linhas.join('\n'));
|
|
@@ -73,7 +73,7 @@ function buscarJSON(url, { timeout = 15000, cabecalhos = {} } = {}) {
|
|
|
73
73
|
política deles pede. */
|
|
74
74
|
function agente() {
|
|
75
75
|
let v = '?';
|
|
76
|
-
try { v = require('
|
|
76
|
+
try { v = require('../../package.json').version; } catch { /* sem package: tanto faz */ }
|
|
77
77
|
return `PrimoCode/${v} (https://github.com/parisgroup-ai/primocode)`;
|
|
78
78
|
}
|
|
79
79
|
|
package/lib/estudio/assets.js
CHANGED
|
@@ -58,7 +58,7 @@ function pegar(url, { json = false, binario = false, cabecalhos = {} } = {}) {
|
|
|
58
58
|
path: u.pathname + u.search,
|
|
59
59
|
headers: {
|
|
60
60
|
// O Openverse recusa quem não se identifica, e com razão.
|
|
61
|
-
'User-Agent': 'PrimoCode-Studio/1.0 (+https://
|
|
61
|
+
'User-Agent': 'PrimoCode-Studio/1.0 (+https://www.npmjs.com/package/primocode)',
|
|
62
62
|
Accept: json ? 'application/json' : '*/*',
|
|
63
63
|
...cabecalhos,
|
|
64
64
|
},
|
|
@@ -36,8 +36,8 @@ const path = require('path');
|
|
|
36
36
|
const https = require('https');
|
|
37
37
|
const referencia = require('../referencia.js');
|
|
38
38
|
const assets = require('./assets.js');
|
|
39
|
-
const marca = require('
|
|
40
|
-
const acervo = require('
|
|
39
|
+
const marca = require('./marca.js');
|
|
40
|
+
const acervo = require('./acervo.js');
|
|
41
41
|
const http = require('http');
|
|
42
42
|
const { URL } = require('url');
|
|
43
43
|
|
package/lib/estudio/index.js
CHANGED
|
@@ -33,7 +33,7 @@ const path = require('path');
|
|
|
33
33
|
const { spawn } = require('child_process');
|
|
34
34
|
|
|
35
35
|
const projeto = require('./projeto.js');
|
|
36
|
-
const acervo = require('
|
|
36
|
+
const acervo = require('./acervo.js');
|
|
37
37
|
const plano = require('./plano.js');
|
|
38
38
|
const remotion = require('./remotion.js');
|
|
39
39
|
const assets = require('./assets.js');
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
const fs = require('fs');
|
|
35
35
|
const path = require('path');
|
|
36
|
-
const referencia = require('
|
|
36
|
+
const referencia = require('../referencia.js');
|
|
37
37
|
|
|
38
38
|
const GENERICAS_FONTE = /^(sans-serif|serif|monospace|system-ui|-apple-system|blinkmacsystemfont|segoe ui|arial|helvetica( neue)?|roboto|inherit|initial|ui-sans-serif|ui-serif|ui-monospace|apple color emoji|segoe ui emoji|noto color emoji|times new roman|courier new|verdana|tahoma|georgia)$/i;
|
|
39
39
|
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -570,6 +570,20 @@ function fontesCSS(plano) {
|
|
|
570
570
|
* `npm install` roda uma vez por projeto e demora minutos — é o preço do
|
|
571
571
|
* Remotion, e está dito no README. Daí em diante, só o render.
|
|
572
572
|
*/
|
|
573
|
+
/**
|
|
574
|
+
* Cita um argumento para a linha de comando do cmd.exe.
|
|
575
|
+
*
|
|
576
|
+
* Só serve ao Windows, e só porque o `shell: true` é obrigatório ali (o Node
|
|
577
|
+
* recusa spawnar npm.cmd/npx.cmd sem ele desde a correção do CVE-2024-27980).
|
|
578
|
+
* Quem não tem espaço nem metacaractere passa intacto: citar tudo deixaria a
|
|
579
|
+
* linha ilegível no log sem ganhar nada.
|
|
580
|
+
*/
|
|
581
|
+
function citarWin(a) {
|
|
582
|
+
const t = String(a);
|
|
583
|
+
if (!/[\s&|<>^"()]/.test(t)) return t;
|
|
584
|
+
return '"' + t.replace(/"/g, '""') + '"';
|
|
585
|
+
}
|
|
586
|
+
|
|
573
587
|
function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
|
|
574
588
|
return new Promise((resolve) => {
|
|
575
589
|
const temNodeModules = fs.existsSync(path.join(raiz, 'node_modules'));
|
|
@@ -582,7 +596,17 @@ function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
|
|
|
582
596
|
aoFalar(i === 0 && !temNodeModules
|
|
583
597
|
? 'instalando o Remotion nesta pasta (uma vez só, alguns minutos)'
|
|
584
598
|
: 'renderizando quadro a quadro');
|
|
585
|
-
|
|
599
|
+
/* NO WINDOWS O `shell: true` NÃO CITA NADA.
|
|
600
|
+
Ele monta a linha do cmd.exe concatenando os argumentos crus, e
|
|
601
|
+
o `saida` é um caminho de arquivo que quase sempre tem espaço
|
|
602
|
+
(C:\Users\Joel Silva\primocode\...\saida\video.mp4). Sem
|
|
603
|
+
aspas, aquilo vira dois argumentos e o render falha — ou, pior,
|
|
604
|
+
escreve num lugar que ninguém pediu.
|
|
605
|
+
O `shell: true` não dá para tirar: desde a correção do
|
|
606
|
+
CVE-2024-27980 o Node recusa spawnar npm.cmd/npx.cmd sem shell.
|
|
607
|
+
Então as aspas entram na mão, só onde o shell existe. */
|
|
608
|
+
const citados = process.platform === 'win32' ? args.map(citarWin) : args;
|
|
609
|
+
const p = spawn(cmd, citados, { cwd: raiz, shell: process.platform === 'win32' });
|
|
586
610
|
let erro = '';
|
|
587
611
|
p.stderr.on('data', (d) => { erro += d.toString().slice(0, 2000); });
|
|
588
612
|
p.stdout.on('data', (d) => {
|
|
@@ -597,7 +621,21 @@ function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
|
|
|
597
621
|
}
|
|
598
622
|
correr(i + 1);
|
|
599
623
|
});
|
|
600
|
-
p.on('error', (e) => {
|
|
624
|
+
p.on('error', (e) => {
|
|
625
|
+
clearTimeout(relogio);
|
|
626
|
+
/* "spawn npx ENOENT" não diz nada a quem instalou pelo ícone.
|
|
627
|
+
Aplicativo aberto pela interface gráfica não herda o PATH do
|
|
628
|
+
terminal: o node vem por caminho absoluto (lib/instalar-app.js),
|
|
629
|
+
mas o npm e o npx não estão em lugar nenhum que ele enxergue. */
|
|
630
|
+
if (e.code === 'ENOENT') {
|
|
631
|
+
return resolve({ ok: false,
|
|
632
|
+
error: `não achei o "${cmd}" nesta máquina — o Remotion precisa dele para renderizar.`,
|
|
633
|
+
comoResolver: 'instale o Node.js completo em nodejs.org (o npm vem junto). '
|
|
634
|
+
+ 'Se ele já está instalado e isto apareceu com o PrimoCode aberto pelo ícone, '
|
|
635
|
+
+ 'abra pelo terminal: o aplicativo não herda o PATH.' });
|
|
636
|
+
}
|
|
637
|
+
resolve({ ok: false, error: e.message });
|
|
638
|
+
});
|
|
601
639
|
};
|
|
602
640
|
correr(0);
|
|
603
641
|
});
|
package/lib/fala.js
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
'use strict';
|
|
39
39
|
|
|
40
40
|
const { c, actionHeader, friendlyError } = require('./ui');
|
|
41
|
+
const markdown = require('./markdown.js');
|
|
41
42
|
|
|
42
43
|
/* Até este tamanho, o texto que o modelo escreve antes de chamar uma tool é
|
|
43
44
|
guardado e sai de uma vez, como pensamento. Passou disso sem tool à vista,
|
|
@@ -89,10 +90,18 @@ function formatar(texto, marca) {
|
|
|
89
90
|
return linhas(texto).map((l, i) => (i === 0 ? `${marca} ${c.white(l)}` : ` ${c.white(l)}`)).join('\n');
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
/** A resposta final, pronta. Espelha o ❯ de quem perguntou.
|
|
93
|
+
/** A resposta final, pronta. Espelha o ❯ de quem perguntou.
|
|
94
|
+
*
|
|
95
|
+
* Passa pelo desenhador de Markdown pelo mesmo motivo do fluxo: o modelo
|
|
96
|
+
* escreve com negrito, lista, tabela e bloco de código, e sem desenhar isso
|
|
97
|
+
* a resposta chega com a marcação à mostra. `formatar` continua servindo ao
|
|
98
|
+
* que NÃO é resposta do modelo (aviso, erro), que é texto nosso e já vem
|
|
99
|
+
* pronto. */
|
|
93
100
|
function resposta(texto) {
|
|
94
101
|
if (!String(texto || '').trim()) return '';
|
|
95
|
-
const
|
|
102
|
+
const largura = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
103
|
+
const linhasMd = markdown.renderizar(texto, largura);
|
|
104
|
+
const saida = '\n' + linhasMd.map((l, i) => (i === 0 ? `${c.brand('●')} ${l}` : ` ${l}`)).join('\n');
|
|
96
105
|
console.log(saida);
|
|
97
106
|
return saida;
|
|
98
107
|
}
|
|
@@ -120,15 +129,37 @@ const frase = friendlyError;
|
|
|
120
129
|
* recuo, para o bloco continuar alinhado ao marcador.
|
|
121
130
|
*/
|
|
122
131
|
function fluxo() {
|
|
132
|
+
/* A resposta do modelo vem em MARKDOWN — é o que ele escreve, e é o que o
|
|
133
|
+
prompt pede. Antes ela saía crua: "**importante**" aparecia com os
|
|
134
|
+
asteriscos, a tabela virava um monte de barras desalinhadas e o bloco de
|
|
135
|
+
código vinha com as três crases à mostra. Quem lia recebia o material
|
|
136
|
+
bruto e formatava de cabeça.
|
|
137
|
+
|
|
138
|
+
O desenho é por LINHA, não no fim: a resposta continua aparecendo
|
|
139
|
+
enquanto chega. Só a tabela e o bloco de código esperam fechar, porque
|
|
140
|
+
meia tabela na tela é pior que a tabela um segundo depois. */
|
|
123
141
|
let escreveu = false;
|
|
142
|
+
const largura = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
143
|
+
const desenho = markdown.fluxo(largura);
|
|
144
|
+
|
|
145
|
+
const sair = (linhas) => {
|
|
146
|
+
for (const linha of linhas) {
|
|
147
|
+
if (!escreveu) { process.stdout.write('\n' + c.brand('●') + ' '); escreveu = true; }
|
|
148
|
+
else process.stdout.write('\n ');
|
|
149
|
+
process.stdout.write(linha);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
124
153
|
return {
|
|
125
154
|
escrever(delta) {
|
|
126
155
|
const t = String(delta || '');
|
|
127
156
|
if (!t) return;
|
|
128
|
-
|
|
129
|
-
|
|
157
|
+
sair(desenho.pedaco(t));
|
|
158
|
+
},
|
|
159
|
+
fechar() {
|
|
160
|
+
sair(desenho.fim());
|
|
161
|
+
if (escreveu) process.stdout.write('\n');
|
|
130
162
|
},
|
|
131
|
-
fechar() { if (escreveu) process.stdout.write('\n'); },
|
|
132
163
|
escreveu: () => escreveu,
|
|
133
164
|
};
|
|
134
165
|
}
|