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 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>/conectar</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="/tudo">o que ele sabe fazer</button>
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> 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('/attach')} <arquivo> anexa arquivo ao próximo pedido
90
- ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
91
- ${c.brand('/tudo')} lista tudo que ele sabe fazer
92
- ${c.brand('/tree')} [n] árvore do projeto
93
- ${c.brand('/read')} <arquivo> mostra um arquivo
94
- ${c.brand('/run')} <comando> executa no shell do projeto
95
- ${c.brand('/open')} <arquivo|url> abre no navegador
96
- ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
97
- ${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
98
- ${c.brand('/gpt')} usa o GPT (Codex) da SUA conta como motor
99
- ${c.brand('/chave')} <provedor> sua própria chave de API (Claude, Codex)
100
- ${c.brand('/conectar')} entra na sua conta Conecta Primo · ${c.brand('/conta')} mostra o plano
101
- ${c.brand('/conversas')} [n] suas conversas salvas — as mesmas do app e do celular
102
- ${c.brand('/sair')} desconecta este computador
103
- ${c.brand('/status')} checa o servidor
104
- ${c.brand('/name')} <nome> como o Primo te chama
105
- ${c.brand('/mascot')} <estilo> mascote: primo|fox|cat
106
- ${c.brand('/config')} mostra suas preferências
107
- ${c.brand('/clear')} limpa a conversa ${c.brand('/cls')} limpa a tela
108
- ${c.brand('/uninstall')} remove o PrimoCode
109
- ${c.brand('/exit')} sair
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
- ['/tudo', 'tudo que ele sabe fazer'],
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
- ['/chave', 'sua própria chave de API (Claude, Codex)'],
146
- ['/conectar', 'entra na sua conta Conecta Primo'],
147
- ['/conta', 'sua conta, plano e computadores conectados'],
148
- ['/conversas', 'suas conversas salvas, as mesmas do app e do celular'],
149
- ['/sair', 'desconecta este computador'],
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) { process.stdout.write(question); pendingAsk = resolve; return; }
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('/conectar'));
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
- async function comprimirConversa() {
631
- if (!memoria.precisaResumir(state.memoria, state.history)) return;
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
- switch (cmd.toLowerCase()) {
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 'chave': case 'chaves': {
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 /chave claude sk-ant-… guarda a chave'));
813
- console.log(c.dim(' /chave codex remover apaga a que estiver lá'));
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(`/chave ${qual} ${p.prefixo}…`));
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': case 'esforco': case 'esforço': {
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 'tudo':
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': case 'app': {
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('/app --instalar'));
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 'conectar': case 'entrar': case 'login': {
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('/conectar trocar'));
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
- // O código aparece AQUI para a pessoa comparar com o
1167
- // da página. Sem essa comparação, um link mandado por
1168
- // outra pessoa conectaria o computador dela à conta
1169
- // de quem clicou.
1170
- console.log(c.muted(' Confira o código na página: ') + c.bold(c.warn(msg)));
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 'conversas': case 'historico': {
1288
+ case 'chats': {
1185
1289
  if (!conta.atual()) {
1186
- console.log(c.muted(' Não conectado. ') + c.brand('/conectar')
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 /conversas 1 abre a primeira e continua dela.'));
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 'sair': case 'logout': {
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 /conectar.'));
1339
+ + c.dim(' A conta continua intacta; entre de novo com /connect.'));
1236
1340
  break;
1237
1341
  }
1238
1342
 
1239
- case 'conta': {
1343
+ case 'account': {
1240
1344
  const eu = conta.atual();
1241
1345
  if (!eu) {
1242
- console.log(c.muted(' Não conectado. ') + c.brand('/conectar') + c.muted(' para entrar.'));
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('/conectar').then(() => conta.atual()),
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
- if (!closed && !busy) rl.prompt();
1559
- }).catch(() => { state.liberado = true; });
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 Primo, o mesmo do terminal: cabeça de topo arredondado, ombros largos,
86
- * dois pezinhos. Lá ele é feito de glifos de bloco:
85
+ /* O DESENHO VEM DE UM LUGAR SÓ.
87
86
  *
88
- * ▐▛███▜▌ a cabeça
89
- * ▝▜█████▛▘ o corpo, mais largo
90
- * ▘▘ ▝▝ os pés
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
- * Aqui são as mesmas três peças em proporção, e não uma transliteração dos
93
- * glifos: meio bloco de terminal vira escada num ícone de 512px. O que faz
94
- * reconhecer é a SILHUETA — topo redondo, ombro saliente, dois pés —, e ela é
95
- * a mesma.
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 do ícone de
102
- // sistema (22% do lado é o raio que o macOS e o Windows usam hoje).
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 — o mesmo halo do fundo do app.
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.6);
109
- const dy = (y - n * 0.34) / (n * 0.6);
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.11 * (1 - d) * (1 - d));
121
+ if (d < 1) ponto(t, x, y, AZUL, 0.10 * (1 - d) * (1 - d));
112
122
  }
113
123
  }
114
124
 
115
- // Cabeça: topo arredondado, ombros retos embaixo (raio só em cima é o que
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
- // Os dois pés.
124
- caixa(t, 10 * u, 24.6 * u, 4.4 * u, 2.6 * u, 1 * u, AZUL_FUNDO);
125
- caixa(t, 17.6 * u, 24.6 * u, 4.4 * u, 2.6 * u, 1 * u, AZUL_FUNDO);
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
- // Os olhos, no negativo — é o que faz o bloco virar bicho.
128
- caixa(t, 12.4 * u, 10.6 * u, 2.6 * u, 3.2 * u, 1.1 * u, FUNDO);
129
- caixa(t, 17 * u, 10.6 * u, 2.6 * u, 3.2 * u, 1.1 * u, FUNDO);
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
- // A luz de cima: uma faixa clara na testa e no ombro. Sem ela o bicho é
132
- // uma silhueta chapada — é o mesmo bisel do "vidro" do estúdio.
133
- caixa(t, 10.4 * u, 8.2 * u, 11.2 * u, 1.4 * u, 0.7 * u, BRILHO);
134
- caixa(t, 7.6 * u, 18.6 * u, 16.8 * u, 1.2 * u, 0.6 * u, BRILHO);
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
  }
@@ -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.34.0",
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": {