primocode 8.34.0 → 8.35.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/app/index.html +2 -2
- package/bin/primocode.js +196 -72
- package/lib/icone.js +50 -34
- package/lib/memoria-conversa.js +24 -1
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<div class="rotulo">Conversas</div>
|
|
41
41
|
<div class="recentes" id="recentes">
|
|
42
42
|
<div class="vazio-lista" id="listaVazia">
|
|
43
|
-
As conversas aparecem aqui quando você entra na sua conta com <code>/
|
|
43
|
+
As conversas aparecem aqui quando você entra na sua conta com <code>/connect</code>.
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
46
|
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
<p>Escreva do jeito que você falaria. Ele lê a pasta, escreve o código,
|
|
70
70
|
roda o que precisar e conta o que fez.</p>
|
|
71
71
|
<div class="sugestoes">
|
|
72
|
-
<button class="sugestao" data-manda="/
|
|
72
|
+
<button class="sugestao" data-manda="/all">o que ele sabe fazer</button>
|
|
73
73
|
<button class="sugestao" data-manda="/project">meus projetos</button>
|
|
74
74
|
<button class="sugestao" data-manda="/effort">esforço</button>
|
|
75
75
|
<button class="sugestao" data-manda="/help">comandos</button>
|
package/bin/primocode.js
CHANGED
|
@@ -82,31 +82,32 @@ let janelaAberta = null;
|
|
|
82
82
|
|
|
83
83
|
const HELP = `
|
|
84
84
|
${c.bold('Comandos')}
|
|
85
|
-
${c.brand('/dir')} <caminho>
|
|
86
|
-
${c.brand('/project')}
|
|
87
|
-
${c.brand('/mode')}
|
|
88
|
-
${c.brand('/effort')}
|
|
89
|
-
${c.brand('/
|
|
90
|
-
${c.brand('/
|
|
91
|
-
${c.brand('/
|
|
92
|
-
${c.brand('/
|
|
93
|
-
${c.brand('/
|
|
94
|
-
${c.brand('/
|
|
95
|
-
${c.brand('/
|
|
96
|
-
${c.brand('/
|
|
97
|
-
${c.brand('/
|
|
98
|
-
${c.brand('/
|
|
99
|
-
${c.brand('/
|
|
100
|
-
${c.brand('/
|
|
101
|
-
${c.brand('/
|
|
102
|
-
${c.brand('/
|
|
103
|
-
${c.brand('/
|
|
104
|
-
${c.brand('/
|
|
105
|
-
${c.brand('/
|
|
106
|
-
${c.brand('/
|
|
107
|
-
${c.brand('/
|
|
108
|
-
${c.brand('/
|
|
109
|
-
${c.brand('/
|
|
85
|
+
${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
|
|
86
|
+
${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
|
|
87
|
+
${c.brand('/mode')} /mode primocode|chat|auto
|
|
88
|
+
${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
|
|
89
|
+
${c.brand('/compact')} resume a conversa e devolve o contexto
|
|
90
|
+
${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
|
|
91
|
+
${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
|
|
92
|
+
${c.brand('/all')} lista tudo que ele sabe fazer
|
|
93
|
+
${c.brand('/tree')} [n] árvore do projeto
|
|
94
|
+
${c.brand('/read')} <arquivo> mostra um arquivo
|
|
95
|
+
${c.brand('/run')} <comando> executa no shell do projeto
|
|
96
|
+
${c.brand('/open')} <arq|url> abre no navegador
|
|
97
|
+
${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
|
|
98
|
+
${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
|
|
99
|
+
${c.brand('/gpt')} usa o GPT (Codex) da SUA conta como motor
|
|
100
|
+
${c.brand('/key')} <provedor> sua própria chave de API (Claude, GPT)
|
|
101
|
+
${c.brand('/connect')} entra na sua conta · ${c.brand('/account')} mostra o plano
|
|
102
|
+
${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
|
|
103
|
+
${c.brand('/logout')} desconecta este computador da conta
|
|
104
|
+
${c.brand('/status')} checa o servidor
|
|
105
|
+
${c.brand('/name')} <nome> como o Primo te chama
|
|
106
|
+
${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
|
|
107
|
+
${c.brand('/config')} mostra suas preferências
|
|
108
|
+
${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
|
|
109
|
+
${c.brand('/uninstall')} remove o PrimoCode
|
|
110
|
+
${c.brand('/exit')} sai do PrimoCode
|
|
110
111
|
|
|
111
112
|
${c.bold('Modos')}
|
|
112
113
|
${c.warn('Primo Code')} ${c.muted('Agente completo — cria arquivos, roda comandos, abre o navegador')}
|
|
@@ -131,10 +132,11 @@ const SLASH_COMANDOS = [
|
|
|
131
132
|
['/dir', 'trabalhar numa pasta sua'],
|
|
132
133
|
['/mode', 'primocode | chat | auto'],
|
|
133
134
|
['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
|
|
135
|
+
['/compact', 'resume a conversa e devolve o contexto'],
|
|
134
136
|
['/attach', 'anexa arquivo ao pedido'],
|
|
135
137
|
['/detach', 'remove os anexos'],
|
|
136
138
|
['/files', 'lista os anexos'],
|
|
137
|
-
['/
|
|
139
|
+
['/all', 'tudo que ele sabe fazer'],
|
|
138
140
|
['/tree', 'árvore do projeto'],
|
|
139
141
|
['/read', 'mostra um arquivo'],
|
|
140
142
|
['/run', 'roda um comando'],
|
|
@@ -142,11 +144,11 @@ const SLASH_COMANDOS = [
|
|
|
142
144
|
['/rc', 'abre a janela do PrimoCode'],
|
|
143
145
|
['/claude', 'usa o Claude Code da sua conta como motor'],
|
|
144
146
|
['/gpt', 'usa o GPT (Codex) da sua conta como motor'],
|
|
145
|
-
['/
|
|
146
|
-
['/
|
|
147
|
-
['/
|
|
148
|
-
['/
|
|
149
|
-
['/
|
|
147
|
+
['/key', 'sua própria chave de API (Claude, Codex)'],
|
|
148
|
+
['/connect', 'entra na sua conta Conecta Primo'],
|
|
149
|
+
['/account', 'sua conta, plano e computadores conectados'],
|
|
150
|
+
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
151
|
+
['/logout', 'desconecta este computador da conta'],
|
|
150
152
|
['/status', 'checa o servidor'],
|
|
151
153
|
['/name', 'como ele te chama'],
|
|
152
154
|
['/mascot', 'primo | fox | cat'],
|
|
@@ -259,8 +261,22 @@ function makePrompt() {
|
|
|
259
261
|
modeTag = state.mode === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
|
|
260
262
|
}
|
|
261
263
|
const proj = state.currentProject ? c.dim(` ${state.currentProject}`) : '';
|
|
264
|
+
|
|
265
|
+
/* O CONTEXTO na barra, como o pedido pede. Só aparece depois que há
|
|
266
|
+
conversa: um "ctx 0%" na abertura é ruído, e ruído que aparece sempre é
|
|
267
|
+
o que a pessoa aprende a não enxergar — inclusive quando marcar 90%.
|
|
268
|
+
Fica âmbar a partir de 70% e vermelho a partir de 100%, que é quando
|
|
269
|
+
`/compact` deixa de ser conselho e vira o que fazer. */
|
|
270
|
+
const uso = memoria.usoDoContexto(state.memoria, state.history);
|
|
271
|
+
const ctx = uso.pct < 5 ? '' : c.dim(' · ')
|
|
272
|
+
+ (uso.pct >= 100 ? c.err(`ctx ${uso.pct}%`)
|
|
273
|
+
: uso.pct >= 70 ? c.warn(`ctx ${uso.pct}%`)
|
|
274
|
+
: c.dim(`ctx ${uso.pct}%`));
|
|
275
|
+
// O modelo ativo: qual motor está pensando. No padrão é o do servidor.
|
|
276
|
+
const motor = state.engine === 'claude-code' ? c.dim(' · ') + c.dim('claude')
|
|
277
|
+
: state.engine === 'codex' ? c.dim(' · ') + c.dim('gpt') : '';
|
|
262
278
|
const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
|
|
263
|
-
return `\n${c.muted(modeTag)}${proj}${att} ${c.brand('❯')} `;
|
|
279
|
+
return `\n${c.muted(modeTag)}${motor}${ctx}${proj}${att} ${c.brand('❯')} `;
|
|
264
280
|
}
|
|
265
281
|
function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
|
|
266
282
|
|
|
@@ -268,9 +284,31 @@ function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
|
|
|
268
284
|
// — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
|
|
269
285
|
let pendingAsk = null;
|
|
270
286
|
|
|
287
|
+
/* ── O QUE A PESSOA ADIANTA ──────────────────────────────────────────────
|
|
288
|
+
* Enquanto a abertura corre — e ela demora, porque confere o plano no
|
|
289
|
+
* servidor — o que for digitado não é comando nem resposta ainda: a pergunta
|
|
290
|
+
* ainda não apareceu. Fica aqui.
|
|
291
|
+
*
|
|
292
|
+
* Sem isto, esse texto caía como PEDIDO no agente. Medido no pacote
|
|
293
|
+
* publicado: o "2" da escolha da IA virou pergunta no chat e o modelo
|
|
294
|
+
* respondeu "Oi! Como posso ajudar?".
|
|
295
|
+
*
|
|
296
|
+
* E guardar não bastava: preso na fila de comandos, ele nunca chegava à
|
|
297
|
+
* pergunta que veio depois. Por isso o `ask` olha aqui ANTES de esperar uma
|
|
298
|
+
* linha nova — quem digitou 2 antes de ver o menu escolheu a 2, e a escolha
|
|
299
|
+
* dela vale.
|
|
300
|
+
*/
|
|
301
|
+
let segurando = false;
|
|
302
|
+
const adiantadas = [];
|
|
303
|
+
|
|
271
304
|
function ask(question) {
|
|
272
305
|
return new Promise((resolve) => {
|
|
273
|
-
if (rl) {
|
|
306
|
+
if (rl) {
|
|
307
|
+
process.stdout.write(question);
|
|
308
|
+
if (adiantadas.length) { const a = adiantadas.shift(); process.stdout.write(a + '\n'); return resolve(a); }
|
|
309
|
+
pendingAsk = resolve;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
274
312
|
const tmp = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
275
313
|
tmp.question(question, (a) => { tmp.close(); resolve(a); });
|
|
276
314
|
});
|
|
@@ -436,7 +474,7 @@ async function sendPrompt(userText) {
|
|
|
436
474
|
if (state.liberado === false) {
|
|
437
475
|
console.log(c.err(' O PrimoCode pede o plano Premium ou Super.'));
|
|
438
476
|
console.log(c.muted(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
|
|
439
|
-
+ c.muted(' · já assinou? ') + c.brand('/
|
|
477
|
+
+ c.muted(' · já assinou? ') + c.brand('/connect'));
|
|
440
478
|
return;
|
|
441
479
|
}
|
|
442
480
|
|
|
@@ -627,8 +665,12 @@ async function dividirTarefa(pedido) {
|
|
|
627
665
|
* Silencioso quando não há o que fazer: nas primeiras rodadas a conversa é
|
|
628
666
|
* curta e o resumo não pagaria a chamada.
|
|
629
667
|
*/
|
|
630
|
-
|
|
631
|
-
|
|
668
|
+
/* `forcado` é o `/compact`: a pessoa mandou resumir, e mandar resumir tem de
|
|
669
|
+
resumir. Sem isso o comando responderia "nada a fazer" justamente quando
|
|
670
|
+
ela está vendo a IA se perder — e "não fiz nada" é a pior resposta possível
|
|
671
|
+
para quem acabou de pedir socorro. */
|
|
672
|
+
async function comprimirConversa(forcado = false) {
|
|
673
|
+
if (!forcado && !memoria.precisaResumir(state.memoria, state.history)) return;
|
|
632
674
|
const sp = new Spinner('resumindo a conversa').start();
|
|
633
675
|
try {
|
|
634
676
|
state.memoria = await memoria.atualizar(state.memoria, state.history, ({ resumo, mensagens }) =>
|
|
@@ -698,11 +740,49 @@ function motorDaVez() {
|
|
|
698
740
|
|
|
699
741
|
function isSlash(text) { return text.trim().startsWith('/'); }
|
|
700
742
|
|
|
743
|
+
/* ── OS NOMES ANTIGOS ────────────────────────────────────────────────────
|
|
744
|
+
* Os comandos passaram a ser todos em inglês. Metade já era; a outra metade
|
|
745
|
+
* estava em português, e a mistura era o que a especificação chamou de
|
|
746
|
+
* bagunça — `/run` ao lado de `/pasta`, `/tree` ao lado de `/tudo`.
|
|
747
|
+
*
|
|
748
|
+
* O nome antigo NÃO some: ele responde e ensina o novo. Sumir de vez faz quem
|
|
749
|
+
* tem o dedo viciado bater num "comando não existe" e concluir que a
|
|
750
|
+
* atualização quebrou alguma coisa — e essa pessoa não vai procurar no /help,
|
|
751
|
+
* vai desinstalar.
|
|
752
|
+
*
|
|
753
|
+
* Uma tabela, e não um `if` dentro de cada `case`: com dezoito apelidos, o
|
|
754
|
+
* `if` espalhado é o jeito garantido de esquecer um. Aqui, esquecer é não
|
|
755
|
+
* escrever a linha, e a linha some da lista.
|
|
756
|
+
*/
|
|
757
|
+
const RENOMEADOS = {
|
|
758
|
+
// o que abre a janela
|
|
759
|
+
app: 'rc',
|
|
760
|
+
// a conta
|
|
761
|
+
entrar: 'connect', login: 'connect', conectar: 'connect',
|
|
762
|
+
conta: 'account',
|
|
763
|
+
sair: 'exit', // sair é SAIR; desconectar a conta é /logout
|
|
764
|
+
// as chaves de API
|
|
765
|
+
chave: 'key', chaves: 'key',
|
|
766
|
+
// o histórico
|
|
767
|
+
conversas: 'chats', historico: 'chats', histórico: 'chats',
|
|
768
|
+
// o resto
|
|
769
|
+
pasta: 'dir',
|
|
770
|
+
esforco: 'effort', esforço: 'effort',
|
|
771
|
+
tudo: 'all', capacidades: 'all',
|
|
772
|
+
};
|
|
773
|
+
|
|
701
774
|
async function handleSlash(text) {
|
|
702
775
|
const [cmd] = text.trim().slice(1).split(/\s+/);
|
|
703
776
|
const rest = text.trim().slice(1 + cmd.length).trim();
|
|
704
777
|
|
|
705
|
-
|
|
778
|
+
let nome = cmd.toLowerCase();
|
|
779
|
+
if (RENOMEADOS[nome]) {
|
|
780
|
+
const novo = RENOMEADOS[nome];
|
|
781
|
+
console.log(c.muted(' Agora é ') + c.brand('/' + novo) + c.muted('.'));
|
|
782
|
+
nome = novo;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
switch (nome) {
|
|
706
786
|
case 'help': console.log(HELP); break;
|
|
707
787
|
|
|
708
788
|
case 'project': {
|
|
@@ -729,8 +809,7 @@ async function handleSlash(text) {
|
|
|
729
809
|
// Antes só existia /project, que SEMPRE cria em ~/primocode/<nome>. Se a
|
|
730
810
|
// sua pasta já existe em outro lugar, não havia como apontar para ela.
|
|
731
811
|
case 'dir':
|
|
732
|
-
case 'cd':
|
|
733
|
-
case 'pasta': {
|
|
812
|
+
case 'cd': {
|
|
734
813
|
if (!rest) {
|
|
735
814
|
const de = {
|
|
736
815
|
padrao: 'a pasta padrão do PrimoCode',
|
|
@@ -798,7 +877,7 @@ async function handleSlash(text) {
|
|
|
798
877
|
/* A chave de API da própria pessoa. Guardada em ~/.primocode/
|
|
799
878
|
chaves.json com permissão 600 — nunca no config.json, que é o
|
|
800
879
|
arquivo que se cola num chamado de suporte. */
|
|
801
|
-
case '
|
|
880
|
+
case 'key': case 'keys': {
|
|
802
881
|
const [qual, ...resto] = rest.split(/\s+/).filter(Boolean);
|
|
803
882
|
const valor = resto.join(' ');
|
|
804
883
|
|
|
@@ -809,8 +888,8 @@ async function handleSlash(text) {
|
|
|
809
888
|
+ (p.tem ? c.ok(p.disfarce) : c.dim('não configurada'))
|
|
810
889
|
+ c.dim(` ${p.variavel}`));
|
|
811
890
|
}
|
|
812
|
-
console.log(c.dim('\n /
|
|
813
|
-
console.log(c.dim(' /
|
|
891
|
+
console.log(c.dim('\n /key claude sk-ant-… guarda a chave'));
|
|
892
|
+
console.log(c.dim(' /key gpt remover apaga a que estiver lá'));
|
|
814
893
|
console.log(c.muted('\n Sem chave própria, cada motor usa a conta em que você já entrou nele.'));
|
|
815
894
|
break;
|
|
816
895
|
}
|
|
@@ -826,7 +905,7 @@ async function handleSlash(text) {
|
|
|
826
905
|
}
|
|
827
906
|
if (!valor) {
|
|
828
907
|
const p = chaves.PROVEDORES[qual];
|
|
829
|
-
console.log(c.muted(` Uso: `) + c.brand(`/
|
|
908
|
+
console.log(c.muted(` Uso: `) + c.brand(`/key ${qual} ${p.prefixo}…`));
|
|
830
909
|
console.log(c.dim(` Pegue a sua em ${p.onde}`));
|
|
831
910
|
break;
|
|
832
911
|
}
|
|
@@ -889,7 +968,7 @@ async function handleSlash(text) {
|
|
|
889
968
|
// O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
|
|
890
969
|
// Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
|
|
891
970
|
// ("/effort high") aplica direto, para quem já sabe o que quer.
|
|
892
|
-
case 'effort':
|
|
971
|
+
case 'effort': {
|
|
893
972
|
const pedido = rest.toLowerCase().trim();
|
|
894
973
|
if (pedido) {
|
|
895
974
|
if (!effort.NOMES.includes(pedido)) {
|
|
@@ -942,9 +1021,8 @@ async function handleSlash(text) {
|
|
|
942
1021
|
// A lista vem de lib/catalogo.js, e o test/catalogo.test.js cruza aquela
|
|
943
1022
|
// lista com as tools que o tools.execute realmente atende. Ou seja: esta
|
|
944
1023
|
// tela não consegue prometer coisa que o programa não faz.
|
|
945
|
-
case '
|
|
946
|
-
case 'tools':
|
|
947
|
-
case 'capacidades': {
|
|
1024
|
+
case 'all':
|
|
1025
|
+
case 'tools': {
|
|
948
1026
|
const cols = Math.max(40, Math.min(process.stdout.columns || 90, 110));
|
|
949
1027
|
console.log('');
|
|
950
1028
|
for (const g of catalogo.GRUPOS) {
|
|
@@ -1033,6 +1111,29 @@ async function handleSlash(text) {
|
|
|
1033
1111
|
// Limpar aqui não apaga o que já subiu: encerra ESTA conversa e a
|
|
1034
1112
|
// próxima pergunta abre outra. Apagar do histórico é decisão da
|
|
1035
1113
|
// pessoa, e ela toma no app — não de raspão num comando de limpar tela.
|
|
1114
|
+
/* Resume a conversa por ordem da pessoa. Diferente do /clear: aqui
|
|
1115
|
+
nada é esquecido — o que já passou vira resumo e o fim continua
|
|
1116
|
+
cru, então dá para seguir de onde parou. */
|
|
1117
|
+
case 'compact': {
|
|
1118
|
+
if (!state.history.length) {
|
|
1119
|
+
console.log(c.muted(' A conversa está vazia — não há o que resumir.'));
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
const antes = memoria.usoDoContexto(state.memoria, state.history);
|
|
1123
|
+
await comprimirConversa(true);
|
|
1124
|
+
const depois = memoria.usoDoContexto(state.memoria, state.history);
|
|
1125
|
+
if (depois.tokens >= antes.tokens) {
|
|
1126
|
+
// Pode não haver o que dobrar (conversa curta, ou o servidor
|
|
1127
|
+
// recusou). Dizer isso é melhor que fingir que encolheu.
|
|
1128
|
+
console.log(c.muted(' Não deu para encolher mais desta vez.'));
|
|
1129
|
+
} else {
|
|
1130
|
+
console.log(c.ok(' Conversa resumida.')
|
|
1131
|
+
+ c.muted(` ctx ${antes.pct}% → ${depois.pct}%`));
|
|
1132
|
+
}
|
|
1133
|
+
refreshPrompt();
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1036
1137
|
case 'clear': state.history = []; state.memoria = memoria.nova();
|
|
1037
1138
|
state.gravador.zerar(); console.log(c.ok('Conversa limpa.')); break;
|
|
1038
1139
|
case 'cls': process.stdout.write('\x1Bc'); break;
|
|
@@ -1074,10 +1175,7 @@ async function handleSlash(text) {
|
|
|
1074
1175
|
não existe" e acha que a atualização quebrou alguma coisa; o aviso
|
|
1075
1176
|
custa uma linha e ensina o nome novo na hora em que ele é
|
|
1076
1177
|
procurado, que é a única hora em que alguém lê. */
|
|
1077
|
-
case 'rc':
|
|
1078
|
-
if (cmd.toLowerCase() === 'app') {
|
|
1079
|
-
console.log(c.muted(' Agora é ') + c.brand('/rc') + c.muted('. Abrindo…'));
|
|
1080
|
-
}
|
|
1178
|
+
case 'rc': {
|
|
1081
1179
|
// `/rc --instalar` põe o PrimoCode no Dock, no menu Iniciar ou no
|
|
1082
1180
|
// menu de aplicativos, com o ícone do mascote. Uma vez só.
|
|
1083
1181
|
if (/^--?(instalar|install)$/.test(rest)) {
|
|
@@ -1128,7 +1226,7 @@ async function handleSlash(text) {
|
|
|
1128
1226
|
// /rc viraria ruído para quem já instalou de propósito.
|
|
1129
1227
|
if (!require('../lib/instalar-app.js').instalado()) {
|
|
1130
1228
|
console.log(c.dim(' Quer o ícone no ') + c.dim(os.platform() === 'darwin' ? 'Dock' : 'menu Iniciar')
|
|
1131
|
-
+ c.dim('? ') + c.brand('/
|
|
1229
|
+
+ c.dim('? ') + c.brand('/rc --instalar'));
|
|
1132
1230
|
}
|
|
1133
1231
|
} catch (e) {
|
|
1134
1232
|
sp.stop();
|
|
@@ -1139,15 +1237,12 @@ async function handleSlash(text) {
|
|
|
1139
1237
|
|
|
1140
1238
|
/* A conta do Conecta Primo. O login inteiro mora em lib/conta.js;
|
|
1141
1239
|
aqui é a tela dele. */
|
|
1142
|
-
case '
|
|
1143
|
-
if (/^(entrar|login)$/.test(cmd.toLowerCase())) {
|
|
1144
|
-
console.log(c.muted(' Agora é ') + c.brand('/conectar') + c.muted('.'));
|
|
1145
|
-
}
|
|
1240
|
+
case 'connect': {
|
|
1146
1241
|
const jaEsta = conta.atual();
|
|
1147
1242
|
if (jaEsta && rest !== 'trocar') {
|
|
1148
1243
|
console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.uid.slice(0, 12) + '…')
|
|
1149
1244
|
+ c.muted(` · plano ${jaEsta.plano}`));
|
|
1150
|
-
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/
|
|
1245
|
+
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/connect trocar'));
|
|
1151
1246
|
break;
|
|
1152
1247
|
}
|
|
1153
1248
|
if (rest === 'trocar') conta.sair();
|
|
@@ -1163,11 +1258,20 @@ async function handleSlash(text) {
|
|
|
1163
1258
|
console.log(' ' + c.brand(msg));
|
|
1164
1259
|
primeiro = false;
|
|
1165
1260
|
} else {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1261
|
+
/* O código é para DIGITAR na página, e é ele que
|
|
1262
|
+
protege: como não vai no link, quem não está olhando
|
|
1263
|
+
este terminal não tem o que digitar — e um link
|
|
1264
|
+
mandado por outra pessoa não conecta o computador
|
|
1265
|
+
dela à sua conta.
|
|
1266
|
+
|
|
1267
|
+
Por isso ele aparece grande e sozinho, com o traço
|
|
1268
|
+
no meio: o que se lê de uma tela e se digita noutra
|
|
1269
|
+
erra menos partido em dois. */
|
|
1270
|
+
const meio = msg.length === 8 ? msg.slice(0, 4) + '-' + msg.slice(4) : msg;
|
|
1271
|
+
console.log('');
|
|
1272
|
+
console.log(c.muted(' Digite este código na página:'));
|
|
1273
|
+
console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
|
|
1274
|
+
console.log('');
|
|
1171
1275
|
console.log(c.dim(' Esperando você aprovar…'));
|
|
1172
1276
|
}
|
|
1173
1277
|
},
|
|
@@ -1181,9 +1285,9 @@ async function handleSlash(text) {
|
|
|
1181
1285
|
|
|
1182
1286
|
/* O histórico da conta — o MESMO que o app desktop e o celular
|
|
1183
1287
|
mostram. Sem número, lista; com número, abre e continua dali. */
|
|
1184
|
-
case '
|
|
1288
|
+
case 'chats': {
|
|
1185
1289
|
if (!conta.atual()) {
|
|
1186
|
-
console.log(c.muted(' Não conectado. ') + c.brand('/
|
|
1290
|
+
console.log(c.muted(' Não conectado. ') + c.brand('/connect')
|
|
1187
1291
|
+ c.muted(' para ver suas conversas aqui, no app e no celular.'));
|
|
1188
1292
|
break;
|
|
1189
1293
|
}
|
|
@@ -1204,7 +1308,7 @@ async function handleSlash(text) {
|
|
|
1204
1308
|
+ ' ' + c.white(x.titulo.slice(0, 52).padEnd(52))
|
|
1205
1309
|
+ c.dim(`${String(x.mensagens).padStart(3)} msg ${quando}`));
|
|
1206
1310
|
});
|
|
1207
|
-
console.log(c.dim('\n /
|
|
1311
|
+
console.log(c.dim('\n /chats 1 abre a primeira e continua dela.'));
|
|
1208
1312
|
break;
|
|
1209
1313
|
}
|
|
1210
1314
|
const n = Number(rest);
|
|
@@ -1228,18 +1332,18 @@ async function handleSlash(text) {
|
|
|
1228
1332
|
break;
|
|
1229
1333
|
}
|
|
1230
1334
|
|
|
1231
|
-
case '
|
|
1335
|
+
case 'logout': {
|
|
1232
1336
|
if (!conta.atual()) { console.log(c.muted(' Você não está conectado.')); break; }
|
|
1233
1337
|
conta.sair();
|
|
1234
1338
|
console.log(c.ok(' Desconectado deste computador.')
|
|
1235
|
-
+ c.dim(' A conta continua intacta; entre de novo com /
|
|
1339
|
+
+ c.dim(' A conta continua intacta; entre de novo com /connect.'));
|
|
1236
1340
|
break;
|
|
1237
1341
|
}
|
|
1238
1342
|
|
|
1239
|
-
case '
|
|
1343
|
+
case 'account': {
|
|
1240
1344
|
const eu = conta.atual();
|
|
1241
1345
|
if (!eu) {
|
|
1242
|
-
console.log(c.muted(' Não conectado. ') + c.brand('/
|
|
1346
|
+
console.log(c.muted(' Não conectado. ') + c.brand('/connect') + c.muted(' para entrar.'));
|
|
1243
1347
|
break;
|
|
1244
1348
|
}
|
|
1245
1349
|
const sp = new Spinner('conferindo').start();
|
|
@@ -1527,6 +1631,8 @@ async function main() {
|
|
|
1527
1631
|
|
|
1528
1632
|
rl.on('line', (line) => {
|
|
1529
1633
|
if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
|
|
1634
|
+
// Abertura correndo: guarda em vez de mandar ao agente.
|
|
1635
|
+
if (segurando) { adiantadas.push(line); return; }
|
|
1530
1636
|
queue.push(line);
|
|
1531
1637
|
drain();
|
|
1532
1638
|
});
|
|
@@ -1540,13 +1646,25 @@ async function main() {
|
|
|
1540
1646
|
* para pegar. Quem colasse dois comandos perdia o segundo, e quem
|
|
1541
1647
|
* fechasse a entrada saía sem nem o "Até logo". Digitando devagar ninguém
|
|
1542
1648
|
* via — os ouvintes ficam prontos milissegundos depois. */
|
|
1649
|
+
/* A FILA FICA SEGURA ENQUANTO A ABERTURA CORRE.
|
|
1650
|
+
*
|
|
1651
|
+
* `busy` já é o freio do `drain`, e é ele que se usa aqui. Sem isso, tudo
|
|
1652
|
+
* o que a pessoa digitar antes de a pergunta aparecer — e ela demora,
|
|
1653
|
+
* porque confere o plano no servidor — cai como PEDIDO no agente. Medido
|
|
1654
|
+
* no pacote publicado: o "2" da escolha da IA virou uma pergunta no chat,
|
|
1655
|
+
* e o modelo respondeu "Oi! Como posso ajudar?".
|
|
1656
|
+
*
|
|
1657
|
+
* O ouvinte de `line` continua atendendo o `ask` normalmente: ele olha o
|
|
1658
|
+
* `pendingAsk` antes da fila. O que fica retido é só o que não era
|
|
1659
|
+
* resposta a nada. */
|
|
1660
|
+
segurando = true;
|
|
1543
1661
|
boasVindas.rodar({
|
|
1544
1662
|
perguntar: ask,
|
|
1545
1663
|
mostrar: (t) => console.log(t),
|
|
1546
1664
|
cores: c,
|
|
1547
1665
|
interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
1548
1666
|
conectado: () => conta.atual(),
|
|
1549
|
-
conectar: () => handleSlash('/
|
|
1667
|
+
conectar: () => handleSlash('/connect').then(() => conta.atual()),
|
|
1550
1668
|
sessao: () => conta.sessao(),
|
|
1551
1669
|
aplicar: (cmd) => handleSlash(cmd),
|
|
1552
1670
|
}).then((r) => {
|
|
@@ -1555,8 +1673,14 @@ async function main() {
|
|
|
1555
1673
|
// o primocode-server, que tem a chave do provedor.
|
|
1556
1674
|
state.liberado = !r || r.liberado !== false;
|
|
1557
1675
|
state.motivoBloqueio = r && r.motivo;
|
|
1558
|
-
|
|
1559
|
-
|
|
1676
|
+
}).catch(() => { state.liberado = true; }).then(() => {
|
|
1677
|
+
// O que sobrou de adiantado não era resposta de nada: vira comando,
|
|
1678
|
+
// na ordem em que foi digitado. Nada se perde, só esperou a vez.
|
|
1679
|
+
segurando = false;
|
|
1680
|
+
while (adiantadas.length) queue.push(adiantadas.shift());
|
|
1681
|
+
if (queue.length) drain();
|
|
1682
|
+
else if (!closed) rl.prompt();
|
|
1683
|
+
});
|
|
1560
1684
|
}
|
|
1561
1685
|
|
|
1562
1686
|
main().catch((e) => { console.error(c.err(e.message || e)); process.exit(1); });
|
package/lib/icone.js
CHANGED
|
@@ -82,56 +82,72 @@ function caixa(t, x0, y0, l, a, r, cor) {
|
|
|
82
82
|
|
|
83
83
|
// ── o mascote ────────────────────────────────────────────────────────────
|
|
84
84
|
|
|
85
|
-
/* O
|
|
86
|
-
* dois pezinhos. Lá ele é feito de glifos de bloco:
|
|
85
|
+
/* O DESENHO VEM DE UM LUGAR SÓ.
|
|
87
86
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* Antes esta função desenhava um bicho à mão — cabeça arredondada, ombros,
|
|
88
|
+
* dois pés, olhos no negativo — e o app e o site desenhavam OUTRO, a matriz de
|
|
89
|
+
* pixels do logo que o dono mandou. Dois desenhos com o mesmo nome: o ícone no
|
|
90
|
+
* Dock não era o bicho que aparecia dentro do programa.
|
|
91
91
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
|
|
92
|
+
* Agora os dois leem a mesma `MATRIZ` do `app/primo-marca.js`. Mudar o logo é
|
|
93
|
+
* mudar onze linhas de texto, e o Dock, a janela e o site mudam juntos.
|
|
94
|
+
*
|
|
95
|
+
* (Aquele arquivo é escrito para o navegador, mas se pendura no `globalThis`
|
|
96
|
+
* quando não há `window` — então dá para carregá-lo aqui sem uma segunda
|
|
97
|
+
* versão dele.) */
|
|
98
|
+
require('../app/primo-marca.js');
|
|
99
|
+
const MARCA = (typeof globalThis !== 'undefined' && globalThis.PrimoMarca) || null;
|
|
100
|
+
|
|
101
|
+
/** #5ec8ef → [94, 200, 239] */
|
|
102
|
+
function doHex(h) {
|
|
103
|
+
const n = parseInt(String(h).replace('#', ''), 16);
|
|
104
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
function desenharPrimo(n) {
|
|
98
108
|
const t = tela(n);
|
|
99
|
-
const u = n / 32; // a grade do desenho, em pixels
|
|
100
109
|
|
|
101
|
-
// A base: o quadrado escuro do app, com o mesmo arredondado
|
|
102
|
-
//
|
|
110
|
+
// A base: o quadrado escuro do app, com o mesmo arredondado que o macOS e
|
|
111
|
+
// o Windows usam hoje (22% do lado). É o formato de azulejo do estilo
|
|
112
|
+
// pedido — glifo chapado, centrado, sobre cor sólida.
|
|
103
113
|
caixa(t, 0, 0, n, n, n * 0.22, FUNDO);
|
|
104
114
|
|
|
105
|
-
// Um brilho fraco atrás do bicho
|
|
115
|
+
// Um brilho fraco atrás do bicho, para o azulejo não ficar chapado.
|
|
106
116
|
for (let y = 0; y < n; y++) {
|
|
107
117
|
for (let x = 0; x < n; x++) {
|
|
108
|
-
const dx = (x - n / 2) / (n * 0.
|
|
109
|
-
const dy = (y - n * 0.
|
|
118
|
+
const dx = (x - n / 2) / (n * 0.62);
|
|
119
|
+
const dy = (y - n * 0.42) / (n * 0.62);
|
|
110
120
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
111
|
-
if (d < 1) ponto(t, x, y, AZUL, 0.
|
|
121
|
+
if (d < 1) ponto(t, x, y, AZUL, 0.10 * (1 - d) * (1 - d));
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
// dá a silhueta do ▐▛███▜▌ — redondo por cima, quadrado por baixo).
|
|
117
|
-
caixa(t, 9 * u, 7 * u, 14 * u, 11 * u, 3.4 * u, AZUL);
|
|
118
|
-
caixa(t, 9 * u, 14 * u, 14 * u, 4 * u, 0.6 * u, AZUL);
|
|
119
|
-
|
|
120
|
-
// Corpo: mais largo que a cabeça, que é o ombro marcado do desenho.
|
|
121
|
-
caixa(t, 6 * u, 17.6 * u, 20 * u, 7.6 * u, 2.2 * u, AZUL);
|
|
125
|
+
if (!MARCA) return t; // sem a matriz, fica o azulejo
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
const linhas = MARCA.MATRIZ;
|
|
128
|
+
const colunas = linhas[0].length;
|
|
129
|
+
const corpo = doHex(MARCA.CORPO);
|
|
130
|
+
const detalhe = doHex(MARCA.DETALHE);
|
|
126
131
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
/* O bicho ocupa 62% do azulejo. Mais que isso ele encosta na borda
|
|
133
|
+
* arredondada e o ícone fica apertado; menos, e some no tamanho de 16px da
|
|
134
|
+
* barra de tarefas — que é onde ele mais aparece. */
|
|
135
|
+
const lado = Math.floor((n * 0.62) / colunas);
|
|
136
|
+
const largura = lado * colunas;
|
|
137
|
+
const altura = lado * linhas.length;
|
|
138
|
+
const x0 = Math.round((n - largura) / 2);
|
|
139
|
+
const y0 = Math.round((n - altura) / 2);
|
|
130
140
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
for (let f = 0; f < linhas.length; f++) {
|
|
142
|
+
for (let cIdx = 0; cIdx < colunas; cIdx++) {
|
|
143
|
+
const ch = linhas[f][cIdx];
|
|
144
|
+
if (ch !== 'C' && ch !== 'D') continue;
|
|
145
|
+
// Sem raio: pixel art com canto redondo deixa de ser pixel art, e
|
|
146
|
+
// vira um monte de bolinhas quando o quadrado é pequeno.
|
|
147
|
+
caixa(t, x0 + cIdx * lado, y0 + f * lado, lado, lado, 0,
|
|
148
|
+
ch === 'C' ? corpo : detalhe);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
135
151
|
|
|
136
152
|
return t;
|
|
137
153
|
}
|
package/lib/memoria-conversa.js
CHANGED
|
@@ -115,10 +115,33 @@ function paraEnviar(memoria, history) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/** Quanto este jeito economiza — para o /status e para o teste provarem. */
|
|
118
|
+
/* ── O QUANTO DA CONVERSA JÁ ESTÁ NA MESA ────────────────────────────────
|
|
119
|
+
* A barra do terminal mostra isso em porcentagem, e a porcentagem precisa de
|
|
120
|
+
* um teto para ser porcentagem DE alguma coisa.
|
|
121
|
+
*
|
|
122
|
+
* O teto de verdade é do servidor e muda com o modelo e com o minuto (ele
|
|
123
|
+
* divide o orçamento entre prompt, ferramentas e conversa a cada pedido). O
|
|
124
|
+
* CLI não vê esse número, e fingir que vê seria pior que não mostrar: a barra
|
|
125
|
+
* diria 20% no exato momento em que o pedido não coubesse.
|
|
126
|
+
*
|
|
127
|
+
* Então o teto aqui é declarado e é DA CONVERSA: quanto de histórico vale a
|
|
128
|
+
* pena carregar antes de resumir. É uma régua honesta — quem chega perto de
|
|
129
|
+
* 100% está mesmo com a conversa pesada, e `/compact` resolve. */
|
|
130
|
+
const TETO_CONVERSA = 4000;
|
|
131
|
+
|
|
132
|
+
function usoDoContexto(memoria, history) {
|
|
133
|
+
const tokens = tokensDe(paraEnviar(memoria, history));
|
|
134
|
+
return {
|
|
135
|
+
tokens,
|
|
136
|
+
teto: TETO_CONVERSA,
|
|
137
|
+
pct: Math.min(999, Math.round((tokens / TETO_CONVERSA) * 100)),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
function economia(memoria, history) {
|
|
119
142
|
const cheio = tokensDe((history || []).slice(-20));
|
|
120
143
|
const enxuto = tokensDe(paraEnviar(memoria, history));
|
|
121
144
|
return { cheio, enxuto, poupado: Math.max(0, cheio - enxuto) };
|
|
122
145
|
}
|
|
123
146
|
|
|
124
|
-
module.exports = { nova, pendentes, precisaResumir, atualizar, paraEnviar, economia, tokensDe, inicioSeguro, CRUAS, LIMITE_TOKENS };
|
|
147
|
+
module.exports = { nova, pendentes, usoDoContexto, TETO_CONVERSA, precisaResumir, atualizar, paraEnviar, economia, tokensDe, inicioSeguro, CRUAS, LIMITE_TOKENS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.35.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|