primocode 9.5.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 +168 -40
- 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 +21 -3
- package/package.json +2 -2
- package/voz/primo_voz/cursor.py +525 -0
- package/voz/primo_voz/orbe.py +4 -1
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();
|
|
@@ -1573,17 +1688,21 @@ async function handleSlash(text) {
|
|
|
1573
1688
|
porque resposta boa de ler é ruim de ouvir — o trabalho é o
|
|
1574
1689
|
mesmo, o jeito de contar é que muda. */
|
|
1575
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();
|
|
1576
1694
|
const plan = prepararPasta(texto);
|
|
1577
|
-
const resultado = await runAct(
|
|
1695
|
+
const resultado = await runAct(texto, {
|
|
1696
|
+
instrucao: vozModo.COMO_FALAR,
|
|
1578
1697
|
server: state.config.server,
|
|
1579
1698
|
model: state.config.model,
|
|
1580
1699
|
effort: state.config.effort,
|
|
1581
1700
|
token: state.config.token,
|
|
1582
|
-
history: memoria.paraEnviar(state.memoria, state.history),
|
|
1701
|
+
history: memoria.paraEnviar(state.memoria, state.history, contextoDoProjeto()),
|
|
1583
1702
|
projectDir: plan.dir,
|
|
1584
1703
|
projectName: plan.name || state.currentProject,
|
|
1585
1704
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
1586
|
-
autorizar:
|
|
1705
|
+
autorizar: autorizarPorVoz,
|
|
1587
1706
|
controle: state.controle,
|
|
1588
1707
|
});
|
|
1589
1708
|
const resposta = (resultado.assistantMessage && resultado.assistantMessage.content) || '';
|
|
@@ -1591,6 +1710,7 @@ async function handleSlash(text) {
|
|
|
1591
1710
|
// continuar digitando não pode começar do zero.
|
|
1592
1711
|
state.history.push({ role: 'user', content: texto });
|
|
1593
1712
|
state.history.push({ role: 'assistant', content: resposta });
|
|
1713
|
+
resumirDepois();
|
|
1594
1714
|
const balanco = registro.resumo(resultado.registro, { colunas: process.stdout.columns || 80 });
|
|
1595
1715
|
if (balanco) console.log(balanco);
|
|
1596
1716
|
return resposta;
|
|
@@ -1945,6 +2065,14 @@ async function main() {
|
|
|
1945
2065
|
state.workDir = cfg.ROOT_DIR;
|
|
1946
2066
|
state.currentProject = null;
|
|
1947
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
|
+
}
|
|
1948
2076
|
|
|
1949
2077
|
// Modo guardado entre sessões. O padrão é auto: sem ele, um "oi" gasta o
|
|
1950
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 };
|
package/lib/config.js
CHANGED
|
@@ -21,6 +21,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
21
21
|
const HISTORY_FILE = path.join(CONFIG_DIR, 'history.jsonl');
|
|
22
22
|
const CONTA_FILE = path.join(CONFIG_DIR, 'conta.json');
|
|
23
23
|
const GENERAL_MEMORY = path.join(ROOT_DIR, 'memory-geral.md');
|
|
24
|
+
const PROJETO_ATIVO_FILE = path.join(CONFIG_DIR, 'projeto-ativo.json');
|
|
24
25
|
|
|
25
26
|
/* O token da conta, lido DIRETO do arquivo.
|
|
26
27
|
*
|
|
@@ -309,7 +310,35 @@ function appendHistory(entry) {
|
|
|
309
310
|
} catch {}
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
/* ── O PROJETO EM QUE SE ESTAVA ─────────────────────────────────────────
|
|
314
|
+
* "Eu peço o jogo de carro, depois peço pra editar ou continuar, e ele cria
|
|
315
|
+
* outro projeto." Nada sobrevivia entre uma sessão e outra: cada abertura
|
|
316
|
+
* começava em ~/primocode, e o "continua" virava projeto novo. Agora o projeto
|
|
317
|
+
* ativo fica guardado aqui, num arquivo só dele (a config tem lista fechada de
|
|
318
|
+
* campos), e a próxima sessão continua nele. Vale por uma semana: depois
|
|
319
|
+
* disso, abrir o PrimoCode e cair num projeto esquecido seria a surpresa. */
|
|
320
|
+
function lembrarProjeto(dir, origem = 'projeto') {
|
|
321
|
+
try {
|
|
322
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
323
|
+
fs.writeFileSync(PROJETO_ATIVO_FILE, JSON.stringify({ dir, origem, quando: Date.now() }, null, 2), 'utf-8');
|
|
324
|
+
} catch { /* lembrar é conveniência: falhou, a sessão segue igual */ }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function projetoLembrado(maxDias = 7) {
|
|
328
|
+
try {
|
|
329
|
+
const j = JSON.parse(fs.readFileSync(PROJETO_ATIVO_FILE, 'utf-8'));
|
|
330
|
+
if (!j || !j.dir || !fs.existsSync(j.dir) || !fs.statSync(j.dir).isDirectory()) return null;
|
|
331
|
+
if (Date.now() - Number(j.quando || 0) > maxDias * 86400000) return null;
|
|
332
|
+
return { dir: j.dir, origem: j.origem || 'projeto' };
|
|
333
|
+
} catch { return null; }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function esquecerProjeto() {
|
|
337
|
+
try { fs.rmSync(PROJETO_ATIVO_FILE, { force: true }); } catch { /* nada a esquecer */ }
|
|
338
|
+
}
|
|
339
|
+
|
|
312
340
|
module.exports = {
|
|
341
|
+
lembrarProjeto, projetoLembrado, esquecerProjeto, PROJETO_ATIVO_FILE,
|
|
313
342
|
loadConfig, saveConfig, appendHistory,
|
|
314
343
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
315
344
|
ensureStructure, slugify, stripAccents,
|