primocode 9.4.0 → 9.6.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 +2 -2
- package/bin/primocode.js +189 -104
- package/lib/act.js +26 -8
- package/lib/catalogo.js +20 -1
- package/lib/config.js +29 -0
- package/lib/cursor.js +120 -0
- package/lib/memoria-conversa.js +20 -11
- package/lib/tools.js +89 -18
- package/lib/ui.js +3 -0
- package/lib/voz.js +189 -2
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +17 -5
- package/voz/primo_voz/cursor.py +525 -0
- package/voz/primo_voz/orbe.py +237 -122
- package/voz/primo_voz/transcrever.py +106 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.
|
|
1
|
+
# PrimoCode v9.6.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.6.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -59,6 +59,7 @@ const VERSION = pkg.version;
|
|
|
59
59
|
const state = {
|
|
60
60
|
config: cfg.loadConfig(),
|
|
61
61
|
history: [],
|
|
62
|
+
resumindo: null, // o resumo da última rodada, sendo feito em segundo plano
|
|
62
63
|
mode: null, // 'primocode' | 'chat' | 'auto' — vem da config em main()
|
|
63
64
|
modoEfetivo: null, // no auto, o modo que o último pedido usou de fato
|
|
64
65
|
attachedFiles: [],
|
|
@@ -427,20 +428,88 @@ async function confirm(question) {
|
|
|
427
428
|
|
|
428
429
|
// ── Projetos ───────────────────────────────────────────────────────────
|
|
429
430
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
/* ── O NOME DO PROJETO ───────────────────────────────────────────────────
|
|
432
|
+
* O nome saía das cinco primeiras palavras depois de "crie um": "agora deixa o
|
|
433
|
+
* carro mais rápido" virava a pasta `agora-deixa-o-carro-mais`, e "oi primo,
|
|
434
|
+
* faz um site" virava `oi-primo-faz-um-site`. Agora o começo da frase
|
|
435
|
+
* (saudação, o nome dele, verbo de pedido, artigo) é pulado, o assunto vai até
|
|
436
|
+
* a primeira palavra que emenda outra ideia ("com", "que", "usando"…), e
|
|
437
|
+
* sobram até quatro palavras que dizem O QUE é: "jogo de carro", "site
|
|
438
|
+
* padaria", "loja roupas". */
|
|
439
|
+
const ABERTURA = new Set([
|
|
440
|
+
'oi', 'ola', 'ei', 'hey', 'e', 'ai', 'primo', 'bom', 'boa', 'dia', 'tarde', 'noite', 'ok', 'beleza',
|
|
441
|
+
'entao', 'agora', 'tipo', 'por', 'favor', 'pfv', 'pf', 'voce', 'vc', 'pode', 'poderia', 'consegue',
|
|
442
|
+
'crie', 'cria', 'criar', 'faca', 'faz', 'fazer', 'monte', 'monta', 'montar', 'desenvolva', 'desenvolver',
|
|
443
|
+
'construa', 'construir', 'gere', 'gera', 'gerar', 'programe', 'programa', 'escreva', 'escreve',
|
|
444
|
+
'quero', 'queria', 'gostaria', 'preciso', 'precisamos', 'vamos', 'bora', 'me', 'pra', 'para', 'mim',
|
|
445
|
+
'um', 'uma', 'uns', 'umas', 'o', 'a', 'os', 'as', 'de', 'novo', 'nova', 'outro', 'outra', 'projeto',
|
|
446
|
+
'simples', 'pequeno', 'pequena', 'rapido', 'bonito', 'bonita',
|
|
447
|
+
// os verbos de CONTINUAR: sem eles, "agora deixa o carro…" virava nome
|
|
448
|
+
'deixa', 'deixe', 'continua', 'continue', 'edita', 'edite', 'muda', 'mude', 'melhora', 'melhore',
|
|
449
|
+
'adiciona', 'adicione', 'coloca', 'coloque', 'poe', 'bota', 'corrige', 'corrija', 'arruma', 'arrume',
|
|
450
|
+
'ajusta', 'ajuste', 'abre', 'abra', 'termina', 'termine',
|
|
435
451
|
]);
|
|
452
|
+
const EMENDA = new Set(['com', 'que', 'usando', 'onde', 'mas', 'porque', 'igual', 'como', 'sobre',
|
|
453
|
+
'em', 'no', 'na', 'nos', 'nas', 'e', 'tipo', 'estilo', 'feito', 'utilizando', 'mais', 'menos']);
|
|
454
|
+
// Palavras que não dizem o que a coisa É — saem de qualquer posição.
|
|
455
|
+
const DE_ENCHER = new Set(['minha', 'meu', 'minhas', 'meus', 'nossa', 'nosso', 'para', 'pra', 'uma', 'um',
|
|
456
|
+
'simples', 'bonito', 'bonita', 'pequeno', 'pequena', 'legal', 'rapido', 'rapida', 'basico', 'basica',
|
|
457
|
+
'completo', 'completa', 'novo', 'nova']);
|
|
458
|
+
|
|
459
|
+
function semAcento(x) { return String(x || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase(); }
|
|
436
460
|
|
|
437
|
-
// "crie um jogo de carro" -> "jogo de carro"
|
|
438
461
|
function deriveProjectName(text) {
|
|
439
|
-
const
|
|
462
|
+
const palavras = String(text || '').split(/\s+/)
|
|
463
|
+
.map((w) => w.replace(/[^\wáéíóúâêôãõçÁÉÍÓÚÂÊÔÃÕÇ-]/gi, ''))
|
|
464
|
+
.filter(Boolean);
|
|
440
465
|
let i = 0;
|
|
441
|
-
while (i <
|
|
442
|
-
const
|
|
443
|
-
|
|
466
|
+
while (i < palavras.length && ABERTURA.has(semAcento(palavras[i]))) i++;
|
|
467
|
+
const assunto = [];
|
|
468
|
+
for (; i < palavras.length && assunto.length < 6; i++) {
|
|
469
|
+
const w = semAcento(palavras[i]);
|
|
470
|
+
if (assunto.length && EMENDA.has(w)) break;
|
|
471
|
+
if (DE_ENCHER.has(w)) continue;
|
|
472
|
+
assunto.push(palavras[i]);
|
|
473
|
+
}
|
|
474
|
+
while (assunto.length && /^(de|do|da|dos|das)$/i.test(semAcento(assunto[assunto.length - 1]))) assunto.pop();
|
|
475
|
+
const nome = assunto.slice(0, 4).join(' ').trim();
|
|
476
|
+
return nome || 'projeto';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* ── "CRIE UM…" DENTRO DE UM PROJETO ────────────────────────────────────
|
|
480
|
+
* Depois de entrar num projeto, TUDO ia para ele — até "crie um site de
|
|
481
|
+
* receitas" no meio do jogo de carro. E fora dele, qualquer "continua" virava
|
|
482
|
+
* projeto novo. A regra agora: dentro de um projeto, só um pedido que CLARAMENTE
|
|
483
|
+
* abre outra coisa sai dele — verbo de criar + "um/uma/outro/novo", assunto
|
|
484
|
+
* que não tem nada a ver com o projeto atual, e nenhuma referência a ele
|
|
485
|
+
* ("isso", "esse", "nele"). Todo o resto ("continua", "muda a cor", "agora
|
|
486
|
+
* põe fases") fica. */
|
|
487
|
+
const RE_PROJETO_NOVO = /\b(novo projeto|projeto novo|outro projeto|do zero|come[cç]a(r)? (um|uma|outro|outra))\b/;
|
|
488
|
+
const RE_CRIAR_UM = /\b(crie|cria|criar|fa[cç]a|faz|fazer|monte|monta|montar|gere|gera|desenvolva|construa|programe|quero|preciso de)\s+(um|uma|outro|outra|novo|nova)\b/;
|
|
489
|
+
const RE_REFERE_ATUAL = /\b(isso|isto|esse|essa|este|esta|nele|nela|dele|dela|no projeto|nesse|nessa|neste|nesta|continua|continue|edita|edite|muda|mude|melhora|melhore|adiciona|adicione|coloca|coloque|p[oõ]e|corrige|corrija|arruma|arrume|ajusta|ajuste)\b/;
|
|
490
|
+
|
|
491
|
+
function pedeProjetoNovo(texto) {
|
|
492
|
+
const t = semAcento(texto);
|
|
493
|
+
if (RE_PROJETO_NOVO.test(t)) return true;
|
|
494
|
+
if (!RE_CRIAR_UM.test(t) || RE_REFERE_ATUAL.test(t)) return false;
|
|
495
|
+
// Mesmo assunto do projeto atual ("crie uma fase nova pro jogo de carro")
|
|
496
|
+
// é continuação, não projeto novo.
|
|
497
|
+
const atual = new Set(cfg.slugify(state.currentProject || '').split('-').filter((x) => x.length > 2));
|
|
498
|
+
const pedido = cfg.slugify(deriveProjectName(texto)).split('-').filter((x) => x.length > 2);
|
|
499
|
+
return !pedido.some((x) => atual.has(x));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* Pedido de mexer no computador da pessoa (abrir app, clicar, digitar,
|
|
503
|
+
mostrar o cursor). Serve só para mandar a dica das ferramentas desktop_*. */
|
|
504
|
+
const RE_COMPUTADOR = /\b(abr[ae]|abrir|clica|clique|clicar|digita|digite|digitar|mouse|cursor|teclado|meu computador|no (meu )?mac|na (minha )?tela|aplicativo|spotify|safari|finder|whatsapp|mensagens|m[úu]sica|configura[çc][õo]es|ajustes)\b/i;
|
|
505
|
+
function querComputador(texto) { return RE_COMPUTADOR.test(String(texto || '')); }
|
|
506
|
+
|
|
507
|
+
/* Guarda onde se está, para a próxima sessão continuar aqui. */
|
|
508
|
+
function entrarNoProjeto(dir, origem = 'projeto') {
|
|
509
|
+
state.workDir = dir;
|
|
510
|
+
state.currentProject = path.basename(dir);
|
|
511
|
+
state.workDirOrigem = origem;
|
|
512
|
+
cfg.lembrarProjeto(dir, origem);
|
|
444
513
|
}
|
|
445
514
|
|
|
446
515
|
// Pasta em que é seguro deixar um agente criar e editar arquivo. A home e a raiz
|
|
@@ -477,7 +546,10 @@ function ehSoEstudio(texto) {
|
|
|
477
546
|
}
|
|
478
547
|
|
|
479
548
|
function planWorkDir(userText) {
|
|
480
|
-
|
|
549
|
+
const escolhida = ['/dir', 'detectada', 'projeto'].includes(state.workDirOrigem);
|
|
550
|
+
// Pasta escolhida a dedo (/dir, ou dita no pedido) manda sempre. Projeto
|
|
551
|
+
// em andamento manda também — a menos que o pedido abra outra coisa.
|
|
552
|
+
if (escolhida && !(state.workDirOrigem === 'projeto' && pedeProjetoNovo(userText))) {
|
|
481
553
|
return { dir: state.workDir, pending: false, name: state.currentProject };
|
|
482
554
|
}
|
|
483
555
|
// "Quando eu peço um vídeo, não é pra ficar com projeto. Só vai criar
|
|
@@ -493,9 +565,7 @@ function planWorkDir(userText) {
|
|
|
493
565
|
|
|
494
566
|
function createPendingProject(plan) {
|
|
495
567
|
const dir = cfg.createProjectDir(plan.name);
|
|
496
|
-
|
|
497
|
-
state.currentProject = path.basename(dir);
|
|
498
|
-
state.workDirOrigem = 'projeto';
|
|
568
|
+
entrarNoProjeto(dir, 'projeto');
|
|
499
569
|
const rotulo = plan.similar ? 'Continuando em' : 'Projeto novo:';
|
|
500
570
|
console.log(c.ok(` ${rotulo} ${state.currentProject}`) + c.muted(` (${dir})`));
|
|
501
571
|
refreshPrompt();
|
|
@@ -530,9 +600,7 @@ function talvezTrocarDePasta(userText) {
|
|
|
530
600
|
if (path.resolve(achado.caminho) === path.resolve(projectDir())) return; // já está lá
|
|
531
601
|
if (!pastaSegura(achado.caminho)) return; // home ou raiz: só por /dir explícito
|
|
532
602
|
|
|
533
|
-
|
|
534
|
-
state.currentProject = path.basename(achado.caminho);
|
|
535
|
-
state.workDirOrigem = 'detectada';
|
|
603
|
+
entrarNoProjeto(achado.caminho, 'detectada');
|
|
536
604
|
console.log(c.ok(` Trabalhando em ${shortPath(achado.caminho)}`) + c.dim(` (você falou "${candidato}")`));
|
|
537
605
|
refreshPrompt();
|
|
538
606
|
}
|
|
@@ -556,9 +624,7 @@ function prepararPasta(userText) {
|
|
|
556
624
|
// buildMemoryContext deste turno leria a pasta errada e a memória do projeto
|
|
557
625
|
// não carregaria.
|
|
558
626
|
if (!plan.pending && path.resolve(plan.dir) !== path.resolve(projectDir())) {
|
|
559
|
-
|
|
560
|
-
state.currentProject = plan.name || path.basename(plan.dir);
|
|
561
|
-
state.workDirOrigem = 'projeto';
|
|
627
|
+
entrarNoProjeto(plan.dir, 'projeto');
|
|
562
628
|
console.log(c.ok(` Continuando em ${state.currentProject}`) + c.muted(` (${shortPath(plan.dir)})`));
|
|
563
629
|
refreshPrompt();
|
|
564
630
|
}
|
|
@@ -602,6 +668,20 @@ async function autorizarControle(oQueVaiFazer, { primeira } = {}) {
|
|
|
602
668
|
return 'nao';
|
|
603
669
|
}
|
|
604
670
|
|
|
671
|
+
/* ── A PERMISSÃO NO MODO VOZ ─────────────────────────────────────────────
|
|
672
|
+
* No modo voz ninguém está olhando o terminal: a pergunta "1, 2 ou Enter"
|
|
673
|
+
* ficava lá embaixo, o agente parava esperando, e para quem só falava ele
|
|
674
|
+
* simplesmente travava. Abrir o /voice e PEDIR em voz para ele mexer no
|
|
675
|
+
* computador já é a autorização — é o produto funcionando como o Jarvis. Fica
|
|
676
|
+
* dito no terminal, a cada ação, o que ele fez; e fechar o PrimoCode esquece. */
|
|
677
|
+
async function autorizarPorVoz(oQueVaiFazer, { primeira } = {}) {
|
|
678
|
+
if (primeira) {
|
|
679
|
+
console.log(' ' + c.warn('▸▸') + ' ' + c.muted('Modo voz: o Primo vai usar o mouse e o teclado. Você vê o cursor dele na tela.'));
|
|
680
|
+
}
|
|
681
|
+
console.log(' ' + c.brand('●') + ' ' + c.dim(oQueVaiFazer));
|
|
682
|
+
return 'sessao';
|
|
683
|
+
}
|
|
684
|
+
|
|
605
685
|
/* ── A TELA DO MARKETPLACE ───────────────────────────────────────────────
|
|
606
686
|
* O desenho e as teclas vivem em lib/marketplace.js, que é função pura. O que
|
|
607
687
|
* mora aqui é o que precisa do terminal: tirar a caixa de digitar da frente,
|
|
@@ -801,10 +881,9 @@ async function sendPrompt(userText) {
|
|
|
801
881
|
|
|
802
882
|
const plan = prepararPasta(userText);
|
|
803
883
|
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
await comprimirConversa();
|
|
884
|
+
// O resumo da rodada anterior é feito em segundo plano logo depois
|
|
885
|
+
// dela; aqui só se espera ele terminar, para não sair com memória velha.
|
|
886
|
+
await esperarResumo();
|
|
808
887
|
|
|
809
888
|
// O pedido passa por uma cabeça antes de virar trabalho: conserta o
|
|
810
889
|
// que está torto, diz qual é o entregável e o que conta como pronto.
|
|
@@ -842,7 +921,7 @@ async function sendPrompt(userText) {
|
|
|
842
921
|
token: state.config.token,
|
|
843
922
|
// Só o primeiro passo enxerga a conversa: os seguintes recebem o
|
|
844
923
|
// que interessa em `jaFeito`, e conversa a mais só ocupa o teto.
|
|
845
|
-
history: i === 0 ? memoria.paraEnviar(state.memoria, state.history) : [],
|
|
924
|
+
history: i === 0 ? memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto()) : [],
|
|
846
925
|
projectDir: plan.dir,
|
|
847
926
|
projectName: plan.name || state.currentProject,
|
|
848
927
|
// Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
|
|
@@ -852,6 +931,9 @@ async function sendPrompt(userText) {
|
|
|
852
931
|
// lembra da resposta é a sessão.
|
|
853
932
|
autorizar: autorizarControle,
|
|
854
933
|
controle: state.controle,
|
|
934
|
+
// Pedido de mexer no computador leva a dica que libera as
|
|
935
|
+
// ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
|
|
936
|
+
instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
|
|
855
937
|
});
|
|
856
938
|
|
|
857
939
|
r = await rodarPasso();
|
|
@@ -877,6 +959,7 @@ async function sendPrompt(userText) {
|
|
|
877
959
|
if (balanco) console.log(balanco);
|
|
878
960
|
state.history.push({ role: 'user', content: userText });
|
|
879
961
|
state.history.push({ role: 'assistant', content: r.assistantMessage.content });
|
|
962
|
+
resumirDepois();
|
|
880
963
|
state.lastResponse = r.assistantMessage.content;
|
|
881
964
|
cfg.appendHistory({ mode: 'primocode', project: state.currentProject, prompt: userText });
|
|
882
965
|
// Sobe cifrado, atrás, sem segurar o prompt. Se não houver conta ou
|
|
@@ -884,7 +967,7 @@ async function sendPrompt(userText) {
|
|
|
884
967
|
state.gravador.turno(userText, r.assistantMessage.content,
|
|
885
968
|
{ modo: 'primocode', projeto: state.currentProject || null });
|
|
886
969
|
} else {
|
|
887
|
-
await
|
|
970
|
+
await esperarResumo();
|
|
888
971
|
await runChat(prompt, userText);
|
|
889
972
|
}
|
|
890
973
|
|
|
@@ -986,21 +1069,55 @@ async function dividirTarefa(pedido) {
|
|
|
986
1069
|
resumir. Sem isso o comando responderia "nada a fazer" justamente quando
|
|
987
1070
|
ela está vendo a IA se perder — e "não fiz nada" é a pior resposta possível
|
|
988
1071
|
para quem acabou de pedir socorro. */
|
|
989
|
-
async function comprimirConversa(forcado = false) {
|
|
1072
|
+
async function comprimirConversa(forcado = false, { silencioso = false } = {}) {
|
|
990
1073
|
if (!forcado && !memoria.precisaResumir(state.memoria, state.history)) return;
|
|
991
|
-
|
|
1074
|
+
// Em segundo plano não há spinner: ele disputaria a tela com o prompt.
|
|
1075
|
+
const sp = silencioso ? { stop() {} } : new Spinner('resumindo a conversa').start();
|
|
992
1076
|
try {
|
|
993
|
-
|
|
1077
|
+
const conversa = state.history;
|
|
1078
|
+
const nova = await memoria.atualizar(state.memoria, conversa, ({ resumo, mensagens }) =>
|
|
994
1079
|
api.requestResumo(state.config.server, { resumo, mensagens, token: state.config.token })
|
|
995
1080
|
.then((r) => r.resumo));
|
|
1081
|
+
// /clear ou /chats trocaram a conversa enquanto o resumo era feito:
|
|
1082
|
+
// o resumo é da conversa velha e não pode voltar por cima da nova.
|
|
1083
|
+
if (state.history !== conversa) { sp.stop(); return; }
|
|
1084
|
+
state.memoria = nova;
|
|
996
1085
|
sp.stop();
|
|
997
1086
|
const e = memoria.economia(state.memoria, state.history);
|
|
998
|
-
if (e.poupado > 0) console.log(c.dim(` ⎿ conversa resumida · ${e.cheio} → ${e.enxuto} tokens por pedido`));
|
|
1087
|
+
if (!silencioso && e.poupado > 0) console.log(c.dim(` ⎿ conversa resumida · ${e.cheio} → ${e.enxuto} tokens por pedido`));
|
|
999
1088
|
} catch {
|
|
1000
1089
|
sp.stop(); // resumo é economia, não requisito: falhou, segue a vida
|
|
1001
1090
|
}
|
|
1002
1091
|
}
|
|
1003
1092
|
|
|
1093
|
+
/* ── O RESUMO DE CADA RODADA ─────────────────────────────────────────────
|
|
1094
|
+
* "Gere um resumo a cada mensagem, pra na próxima ela ler só esse resumo."
|
|
1095
|
+
* O resumo começa assim que a resposta termina, atrás, sem segurar o prompt —
|
|
1096
|
+
* a pessoa lê a resposta enquanto ele é feito. O pedido seguinte espera por
|
|
1097
|
+
* ele (com teto: rede lenta não pode travar a conversa), e aí leva só o
|
|
1098
|
+
* resumo, não a conversa inteira. Ver lib/memoria-conversa.js. */
|
|
1099
|
+
function resumirDepois() {
|
|
1100
|
+
const anterior = state.resumindo || Promise.resolve();
|
|
1101
|
+
const atual = anterior.then(() => comprimirConversa(false, { silencioso: true })).catch(() => {});
|
|
1102
|
+
state.resumindo = atual;
|
|
1103
|
+
atual.then(() => { if (state.resumindo === atual) state.resumindo = null; });
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
async function esperarResumo(tetoMs = 15000) {
|
|
1107
|
+
if (!state.resumindo) return;
|
|
1108
|
+
await Promise.race([state.resumindo, new Promise((ok) => setTimeout(ok, tetoMs))]);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/* A linha que vai junto do resumo: em que projeto se está. É o que faz
|
|
1112
|
+
"continua o jogo" e "muda a cor" caírem no mesmo projeto em vez de inventar
|
|
1113
|
+
outro. */
|
|
1114
|
+
function contextoDoProjeto() {
|
|
1115
|
+
if (!state.currentProject) return '';
|
|
1116
|
+
return `Projeto atual: ${state.currentProject} (pasta ${shortPath(projectDir())}). `
|
|
1117
|
+
+ 'Pedidos como "continua", "edita", "muda", "melhora" ou "isso" são sobre ELE: '
|
|
1118
|
+
+ 'trabalhe nos arquivos que já existem lá, não crie outro projeto.';
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1004
1121
|
// O chat também precisa parar no Esc Esc. Sem isto o texto continuava saindo
|
|
1005
1122
|
// na tela depois de o usuário ter mandado parar.
|
|
1006
1123
|
let chatEmVoo = null;
|
|
@@ -1027,7 +1144,7 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
1027
1144
|
try {
|
|
1028
1145
|
await api.requestChat(state.config.server, {
|
|
1029
1146
|
prompt: promptForModel,
|
|
1030
|
-
history: memoria.paraEnviar(state.memoria, state.history),
|
|
1147
|
+
history: memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto()),
|
|
1031
1148
|
model: state.config.model,
|
|
1032
1149
|
token: state.config.token,
|
|
1033
1150
|
sinal: chatEmVoo.signal,
|
|
@@ -1057,6 +1174,7 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
1057
1174
|
if (text) {
|
|
1058
1175
|
state.history.push({ role: 'user', content: userTextForMemory });
|
|
1059
1176
|
state.history.push({ role: 'assistant', content: text });
|
|
1177
|
+
resumirDepois();
|
|
1060
1178
|
state.lastResponse = text;
|
|
1061
1179
|
cfg.appendHistory({ mode: 'chat', prompt: userTextForMemory });
|
|
1062
1180
|
state.gravador.turno(userTextForMemory, text, { modo: 'chat' });
|
|
@@ -1121,6 +1239,7 @@ async function rodarBash(text) {
|
|
|
1121
1239
|
role: 'assistant',
|
|
1122
1240
|
content: `Saída (código ${r.code ?? 0}):\n${saida.slice(0, 4000) || '(vazia)'}`,
|
|
1123
1241
|
});
|
|
1242
|
+
resumirDepois();
|
|
1124
1243
|
}
|
|
1125
1244
|
|
|
1126
1245
|
/* ── OS NOMES ANTIGOS ────────────────────────────────────────────────────
|
|
@@ -1179,9 +1298,7 @@ async function handleSlash(text) {
|
|
|
1179
1298
|
break;
|
|
1180
1299
|
}
|
|
1181
1300
|
const dir = cfg.createProjectDir(rest);
|
|
1182
|
-
|
|
1183
|
-
state.currentProject = path.basename(dir);
|
|
1184
|
-
state.workDirOrigem = 'projeto';
|
|
1301
|
+
entrarNoProjeto(dir, 'projeto');
|
|
1185
1302
|
console.log(c.ok(`Projeto: ${state.currentProject}`) + c.muted(` (${dir})`));
|
|
1186
1303
|
refreshPrompt();
|
|
1187
1304
|
break;
|
|
@@ -1232,9 +1349,7 @@ async function handleSlash(text) {
|
|
|
1232
1349
|
}
|
|
1233
1350
|
}
|
|
1234
1351
|
|
|
1235
|
-
|
|
1236
|
-
state.currentProject = path.basename(alvo);
|
|
1237
|
-
state.workDirOrigem = '/dir';
|
|
1352
|
+
entrarNoProjeto(alvo, '/dir');
|
|
1238
1353
|
const quantos = (() => { try { return fs.readdirSync(alvo).length; } catch { return 0; } })();
|
|
1239
1354
|
console.log(c.ok(` Trabalhando em ${shortPath(alvo)}`) + c.muted(` (${quantos} ${quantos === 1 ? 'item' : 'itens'})`));
|
|
1240
1355
|
refreshPrompt();
|
|
@@ -1523,72 +1638,29 @@ async function handleSlash(text) {
|
|
|
1523
1638
|
break;
|
|
1524
1639
|
}
|
|
1525
1640
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1641
|
+
/* ── PREPARAR TUDO, SEM PERGUNTAR ───────────────────────────
|
|
1642
|
+
"Instale tudo que precisa, rode tudo que precisa." O que falta
|
|
1643
|
+
(bibliotecas do sistema, pacotes do Python, o Whisper local e o
|
|
1644
|
+
reconhecedor do "ei, primo") é instalado aqui, com uma linha de
|
|
1645
|
+
progresso por etapa. Da segunda vez em diante, tudo já está lá
|
|
1646
|
+
e isto leva um segundo. Ver `preparar` em lib/voz.js.
|
|
1647
|
+
|
|
1648
|
+
O reconhecedor do nome é o que deixa o microfone aberto sem o
|
|
1649
|
+
áudio sair da máquina: o nome é reconhecido AQUI. Se ele não
|
|
1650
|
+
ficar pronto, o modo voz abre já ouvindo — nunca manda o dia
|
|
1651
|
+
inteiro de áudio para o servidor. */
|
|
1652
|
+
const sp = new Spinner('preparando o modo voz').start();
|
|
1653
|
+
const prep = await vozModo.preparar((msg) => { sp.text = msg; });
|
|
1654
|
+
sp.stop();
|
|
1655
|
+
if (!prep.ok) {
|
|
1656
|
+
console.log(c.err(` ${prep.error}`));
|
|
1657
|
+
if (prep.comoResolver) console.log(c.muted(` ${prep.comoResolver}`));
|
|
1658
|
+
for (const a of prep.avisos) console.log(c.dim(` ${a}`));
|
|
1537
1659
|
break;
|
|
1538
1660
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
if (!(await confirm('Instalar agora?'))) {
|
|
1543
|
-
console.log(c.muted(' Sem eles o modo voz não abre.'));
|
|
1544
|
-
if (pronto.comoResolver) console.log(c.dim(` ${pronto.comoResolver}`));
|
|
1545
|
-
break;
|
|
1546
|
-
}
|
|
1547
|
-
const sp = new Spinner('instalando').start();
|
|
1548
|
-
const r = await vozModo.instalar(pronto.python, pronto.instalavel, () => {});
|
|
1549
|
-
sp.stop();
|
|
1550
|
-
if (!r.ok) {
|
|
1551
|
-
console.log(c.err(` não deu: ${r.error}`));
|
|
1552
|
-
console.log(c.muted(` ${pronto.comoResolver}`));
|
|
1553
|
-
break;
|
|
1554
|
-
}
|
|
1555
|
-
const conferido = vozModo.conferir();
|
|
1556
|
-
if (!conferido.ok) {
|
|
1557
|
-
console.log(c.err(` ainda falta: ${conferido.error}`));
|
|
1558
|
-
break;
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
const py = (pronto.ok ? pronto : vozModo.conferir()).python;
|
|
1563
|
-
|
|
1564
|
-
/* ── O RECONHECEDOR LOCAL ────────────────────────────────────
|
|
1565
|
-
Ele é o que permite o microfone ficar aberto sem o áudio sair
|
|
1566
|
-
da máquina: o nome é reconhecido AQUI, e só depois disso a
|
|
1567
|
-
frase viaja. São ~31 MB, uma vez, e ficam em ~/.primocode.
|
|
1568
|
-
|
|
1569
|
-
Se a pessoa recusar, o modo voz ainda abre — mas já ouvindo,
|
|
1570
|
-
sem espera. O que NÃO acontece em hipótese nenhuma é cair para
|
|
1571
|
-
"manda tudo para o servidor o dia inteiro": isso seria trocar
|
|
1572
|
-
a privacidade dela por conveniência sem perguntar. */
|
|
1573
|
-
const espera = await vozModo.conferirEspera(py);
|
|
1574
|
-
let esperando = espera.ok;
|
|
1575
|
-
if (!espera.ok && espera.baixavel) {
|
|
1576
|
-
console.log('');
|
|
1577
|
-
console.log(' ' + c.muted('Para ficar ouvindo só o nome, sem mandar áudio para lugar nenhum,'));
|
|
1578
|
-
console.log(' ' + c.muted('ele precisa do reconhecedor local — ') + c.dim('~31 MB, uma vez.'));
|
|
1579
|
-
if (await confirm('Baixar agora?')) {
|
|
1580
|
-
const sp = new Spinner('baixando o reconhecedor').start();
|
|
1581
|
-
const r = await vozModo.baixarEspera(py);
|
|
1582
|
-
sp.stop();
|
|
1583
|
-
if (r.ok) esperando = true;
|
|
1584
|
-
else {
|
|
1585
|
-
console.log(c.err(` não deu: ${r.error}`));
|
|
1586
|
-
if (r.comoResolver) console.log(c.dim(` ${r.comoResolver}`));
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
} else if (!espera.ok && espera.error) {
|
|
1590
|
-
console.log(c.dim(` ${espera.error}`));
|
|
1591
|
-
}
|
|
1661
|
+
for (const a of prep.avisos) console.log(c.dim(` ${a}`));
|
|
1662
|
+
const py = prep.python;
|
|
1663
|
+
const esperando = prep.esperando;
|
|
1592
1664
|
|
|
1593
1665
|
console.log('');
|
|
1594
1666
|
console.log(' ' + c.bold(c.brand('Primo Code Voice')));
|
|
@@ -1616,17 +1688,21 @@ async function handleSlash(text) {
|
|
|
1616
1688
|
porque resposta boa de ler é ruim de ouvir — o trabalho é o
|
|
1617
1689
|
mesmo, o jeito de contar é que muda. */
|
|
1618
1690
|
pensar: async (texto) => {
|
|
1691
|
+
// Mesma memória do terminal: espera o resumo da rodada
|
|
1692
|
+
// anterior e leva só ele, com o projeto atual.
|
|
1693
|
+
await esperarResumo();
|
|
1619
1694
|
const plan = prepararPasta(texto);
|
|
1620
|
-
const resultado = await runAct(
|
|
1695
|
+
const resultado = await runAct(texto, {
|
|
1696
|
+
instrucao: vozModo.COMO_FALAR,
|
|
1621
1697
|
server: state.config.server,
|
|
1622
1698
|
model: state.config.model,
|
|
1623
1699
|
effort: state.config.effort,
|
|
1624
1700
|
token: state.config.token,
|
|
1625
|
-
history: memoria.paraEnviar(state.memoria, state.history),
|
|
1701
|
+
history: memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto()),
|
|
1626
1702
|
projectDir: plan.dir,
|
|
1627
1703
|
projectName: plan.name || state.currentProject,
|
|
1628
1704
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
1629
|
-
autorizar:
|
|
1705
|
+
autorizar: autorizarPorVoz,
|
|
1630
1706
|
controle: state.controle,
|
|
1631
1707
|
});
|
|
1632
1708
|
const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
|
|
@@ -1634,6 +1710,7 @@ async function handleSlash(text) {
|
|
|
1634
1710
|
// continuar digitando não pode começar do zero.
|
|
1635
1711
|
state.history.push({ role: 'user', content: texto });
|
|
1636
1712
|
state.history.push({ role: 'assistant', content: resposta });
|
|
1713
|
+
resumirDepois();
|
|
1637
1714
|
const balanco = registro.resumo(resultado.registro, { colunas: process.stdout.columns || 80 });
|
|
1638
1715
|
if (balanco) console.log(balanco);
|
|
1639
1716
|
return resposta;
|
|
@@ -1988,6 +2065,14 @@ async function main() {
|
|
|
1988
2065
|
state.workDir = cfg.ROOT_DIR;
|
|
1989
2066
|
state.currentProject = null;
|
|
1990
2067
|
state.workDirOrigem = 'padrao';
|
|
2068
|
+
// O projeto em que se estava na última sessão (até uma semana atrás): o
|
|
2069
|
+
// "continua o jogo" de amanhã cai nele, e não num projeto novo.
|
|
2070
|
+
const lembrado = cfg.projetoLembrado();
|
|
2071
|
+
if (lembrado && pastaSegura(lembrado.dir)) {
|
|
2072
|
+
state.workDir = lembrado.dir;
|
|
2073
|
+
state.currentProject = path.basename(lembrado.dir);
|
|
2074
|
+
state.workDirOrigem = lembrado.origem === '/dir' ? '/dir' : 'projeto';
|
|
2075
|
+
}
|
|
1991
2076
|
|
|
1992
2077
|
// Modo guardado entre sessões. O padrão é auto: sem ele, um "oi" gasta o
|
|
1993
2078
|
// mesmo que uma refatoração.
|
package/lib/act.js
CHANGED
|
@@ -454,6 +454,12 @@ async function runAct(userPrompt, opts) {
|
|
|
454
454
|
`test/repeticao.js` e `test/continuar.js` teriam de falar com a
|
|
455
455
|
internet para verificar o loop do agente. */
|
|
456
456
|
motor = api,
|
|
457
|
+
/* Instrução de FORMA que vai junto do pedido mas não É o pedido — a
|
|
458
|
+
do modo voz ("responda em três frases faladas"). Ela chega ao
|
|
459
|
+
modelo; a memória e o histórico guardam só o que a pessoa disse.
|
|
460
|
+
Antes ela ia colada no texto, e a memória do projeto enchia de
|
|
461
|
+
"[MODO VOZ] A sua resposta vai ser lida em voz alta…". */
|
|
462
|
+
instrucao = '',
|
|
457
463
|
} = opts || {};
|
|
458
464
|
|
|
459
465
|
// A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
|
|
@@ -463,6 +469,16 @@ async function runAct(userPrompt, opts) {
|
|
|
463
469
|
let criarPasta = ensureWorkDir || null;
|
|
464
470
|
let pastaAtual = projectDir; // onde as tools agem AGORA
|
|
465
471
|
let temPasta = !criarPasta; // já é a pasta definitiva?
|
|
472
|
+
/* Cria a pasta do projeto pendente, uma vez. Chamado antes de qualquer
|
|
473
|
+
ferramenta que faz código (ver `criaProjeto` no catálogo) — e ANTES do
|
|
474
|
+
subagente e do pipeline, que senão escreviam na raiz de ~/primocode. */
|
|
475
|
+
const garantirPasta = () => {
|
|
476
|
+
if (!criarPasta) return;
|
|
477
|
+
const nova = criarPasta();
|
|
478
|
+
if (nova) pastaAtual = nova;
|
|
479
|
+
criarPasta = null;
|
|
480
|
+
temPasta = true;
|
|
481
|
+
};
|
|
466
482
|
const falhasPendentes = [];
|
|
467
483
|
let tentativasDeEncerrar = 0;
|
|
468
484
|
// Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
|
|
@@ -492,6 +508,7 @@ async function runAct(userPrompt, opts) {
|
|
|
492
508
|
if (!subagent) {
|
|
493
509
|
const comSkills = skills.montar(userPrompt);
|
|
494
510
|
pedido = comSkills.texto;
|
|
511
|
+
if (instrucao) pedido = `${instrucao}\n\nPEDIDO: ${pedido}`;
|
|
495
512
|
if (comSkills.skills.length) {
|
|
496
513
|
fala.pensamento(`seguindo: ${comSkills.skills.map((s) => s.titulo).join(', ')}`, profundidade);
|
|
497
514
|
}
|
|
@@ -627,7 +644,7 @@ async function runAct(userPrompt, opts) {
|
|
|
627
644
|
|
|
628
645
|
emVoo = new AbortController();
|
|
629
646
|
try {
|
|
630
|
-
await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir, projectName, subagent, sinal: emVoo.signal }, (evt) => {
|
|
647
|
+
await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
|
|
631
648
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
632
649
|
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
633
650
|
if (evt.delta) {
|
|
@@ -843,6 +860,7 @@ async function runAct(userPrompt, opts) {
|
|
|
843
860
|
// sinal de decomposição ruim, e o excedente roda em série mesmo.
|
|
844
861
|
const spawns = toolCalls.filter((tc) => tc.name === 'spawn_agent');
|
|
845
862
|
const resultadosSpawn = new Map();
|
|
863
|
+
if (spawns.length && !PARAR) garantirPasta();
|
|
846
864
|
if (spawns.length > 1 && !PARAR) {
|
|
847
865
|
const juntos = spawns.slice(0, 4);
|
|
848
866
|
fala.paralelo(juntos.length, profundidade);
|
|
@@ -906,6 +924,7 @@ async function runAct(userPrompt, opts) {
|
|
|
906
924
|
esquecer de passar o token ou a pasta. */
|
|
907
925
|
if (tc.name === 'construir_app') {
|
|
908
926
|
algoRodou = true;
|
|
927
|
+
garantirPasta();
|
|
909
928
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
910
929
|
const r = await rodarPipeline(tc.arguments || {}, {
|
|
911
930
|
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, livro, autorizar, controle: portaoControle,
|
|
@@ -928,12 +947,7 @@ async function runAct(userPrompt, opts) {
|
|
|
928
947
|
continue;
|
|
929
948
|
}
|
|
930
949
|
|
|
931
|
-
if (criarPasta && catalogo.
|
|
932
|
-
const nova = criarPasta();
|
|
933
|
-
if (nova) pastaAtual = nova;
|
|
934
|
-
criarPasta = null;
|
|
935
|
-
temPasta = true;
|
|
936
|
-
}
|
|
950
|
+
if (criarPasta && catalogo.criaProjeto(tc.name, tc.arguments)) garantirPasta();
|
|
937
951
|
|
|
938
952
|
if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
|
|
939
953
|
voltasEmFalso++;
|
|
@@ -1209,9 +1223,13 @@ async function rodarPipeline(args, ctx) {
|
|
|
1209
1223
|
const path = require('path');
|
|
1210
1224
|
const pedido = args.pedido || args.prompt || '';
|
|
1211
1225
|
const base = projectDir || cfg.ROOT_DIR;
|
|
1226
|
+
/* Dentro de um projeto, o app é montado NA pasta dele. A subpasta com
|
|
1227
|
+
nome derivado do pedido só vale na raiz: era ela que fazia o app nascer
|
|
1228
|
+
num lugar que a conversa nunca mais achava. */
|
|
1229
|
+
const naRaiz = path.resolve(base) === path.resolve(cfg.ROOT_DIR);
|
|
1212
1230
|
const pasta = args.pasta
|
|
1213
1231
|
? path.resolve(base, args.pasta)
|
|
1214
|
-
: pipeline.pastaPara(pedido, base);
|
|
1232
|
+
: (naRaiz ? pipeline.pastaPara(pedido, base) : base);
|
|
1215
1233
|
|
|
1216
1234
|
const correr = async (prompt, rotulo) => {
|
|
1217
1235
|
if (PARAR) return '';
|
package/lib/catalogo.js
CHANGED
|
@@ -206,6 +206,25 @@ function escreveArquivo(tool) {
|
|
|
206
206
|
return ESCREVEM.has(tool);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/* ── O QUE FAZ NASCER UMA PASTA DE PROJETO ──────────────────────────────
|
|
210
|
+
* "Ela deve saber quando criar pasta do projeto e quando não criar — só vai
|
|
211
|
+
* criar quando fizer código." `escreve` era largo demais para isso: um print
|
|
212
|
+
* de tela, uma narração do estúdio e um vídeo também escrevem arquivo, e cada
|
|
213
|
+
* um deles criava um projeto com o nome da frase. Aqui só o que é CÓDIGO:
|
|
214
|
+
* escrever ou editar arquivo, montar um app, delegar a um especialista, e o
|
|
215
|
+
* comando de terminal que cria arquivo (o agente que faz `npm create` ou
|
|
216
|
+
* `mkdir` estava pondo o projeto solto na raiz de ~/primocode). */
|
|
217
|
+
const CRIAM_PROJETO = new Set(['write_file', 'edit_file', 'multi_edit', 'construir_app', 'spawn_agent']);
|
|
218
|
+
const SHELL_QUE_CRIA = /\b(mkdir|touch|git\s+clone|npm\s+(init|create)|npx\s+(create-|degit|nuxi\s+init)|yarn\s+create|pnpm\s+(create|dlx\s+create-)|bun\s+create|cargo\s+(new|init)|python3?\s+-m\s+venv|django-admin\s+startproject|rails\s+new|flutter\s+create|dotnet\s+new|composer\s+create-project|expo\s+init)\b|(^|[^<>&|0-9])>>?\s*[\w./~-]/;
|
|
219
|
+
|
|
220
|
+
function criaProjeto(tool, args) {
|
|
221
|
+
if (CRIAM_PROJETO.has(tool)) return true;
|
|
222
|
+
if (tool === 'run_shell' || tool === 'run_in_new_terminal') {
|
|
223
|
+
return SHELL_QUE_CRIA.test(String((args && (args.command || args.comando)) || ''));
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
209
228
|
|
|
210
229
|
/**
|
|
211
230
|
* As ferramentas de nome mais parecido com o que foi chamado.
|
|
@@ -244,5 +263,5 @@ function parecidasCom(nome) {
|
|
|
244
263
|
return pontos.slice(0, 6).map((x) => x.t);
|
|
245
264
|
}
|
|
246
265
|
|
|
247
|
-
module.exports = {
|
|
266
|
+
module.exports = { criaProjeto,
|
|
248
267
|
parecidasCom, GRUPOS, TODAS, ESCREVEM, escreveArquivo };
|