primocode 10.7.0 → 10.8.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 CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.7.0
1
+ # PrimoCode v10.8.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 v10.7.0
12
+ ▐▛███▜▌ Primo Code v10.8.0
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/bin/primocode.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * PrimoCode CLI · ConectaPrimoAI
3
+ * PrimoCode CLI
4
4
  * Modos: primocode (agente completo, PADRÃO) · chat (conversa pura)
5
5
  * Cada sessão trabalha dentro de uma pasta de projeto em ~/primocode/.
6
6
  */
@@ -42,6 +42,7 @@ const marketplace = require('../lib/marketplace');
42
42
  const portaoSkill = require('../lib/portao-skill');
43
43
  const vozModo = require('../lib/voz');
44
44
  const conta = require('../lib/conta');
45
+ const planosTela = require('../lib/planos.js');
45
46
  const nuvem = require('../lib/nuvem');
46
47
  const compat = require('../lib/compatibilidade');
47
48
  const mencoesLib = require('../lib/mencoes');
@@ -141,6 +142,7 @@ ${c.bold('Comandos')}
141
142
  ${c.brand('/copiar')} copia a última resposta
142
143
  ${c.brand('/historico')} [termo] procura nos seus pedidos antigos
143
144
  ${c.brand('/login')} entra na sua conta · ${c.brand('/account')} mostra o plano
145
+ ${c.brand('/planos')} os planos e o link para assinar
144
146
  ${c.brand('/chats')} [n] suas conversas — as mesmas do app e do celular
145
147
  ${c.brand('/logout')} desconecta este computador da conta
146
148
  ${c.brand('/comandos')} seus comandos (/revisar, /explicar…) · /comandos novo <nome>
@@ -196,8 +198,9 @@ const SLASH_COMANDOS = [
196
198
  ['/abrir', 'abre o projeto atual: o servidor de pé, ou a index.html'],
197
199
  ['/copiar', 'copia a última resposta'],
198
200
  ['/historico', 'procura nos seus pedidos antigos: /historico login'],
199
- ['/login', 'entra na sua conta Conecta Primo'],
200
- ['/account', 'sua conta, plano e computadores conectados'],
201
+ ['/login', 'entra na sua conta (abre o navegador) · /login senha: e-mail e senha aqui'],
202
+ ['/account', 'sua conta e o seu plano'],
203
+ ['/planos', 'os planos do PrimoCode e o link para assinar'],
201
204
  ['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
202
205
  ['/logout', 'desconecta este computador da conta'],
203
206
  ['!comando', 'roda direto no shell, sem passar pelo agente (o campo fica âmbar)'],
@@ -1131,9 +1134,9 @@ async function sendPrompt(userText) {
1131
1134
  que houve e como resolver, em vez de deixar a pessoa achando que o
1132
1135
  produto está quebrado. */
1133
1136
  if (state.liberado === false) {
1134
- console.log(c.err(' O PrimoCode pede o plano Premium ou Super.'));
1135
- console.log(c.muted(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
1136
- + c.muted(' · já assinou? ') + c.brand('/login'));
1137
+ console.log(c.err(' O PrimoCode pede um plano (a partir de R$ 69,90/mês).'));
1138
+ console.log(c.muted(' Veja e assine com ') + c.brand('/planos')
1139
+ + c.muted(' · já assinou? ') + c.brand('/account'));
1137
1140
  return;
1138
1141
  }
1139
1142
 
@@ -1736,6 +1739,7 @@ const RENOMEADOS = {
1736
1739
  // a conta
1737
1740
  entrar: 'login', conectar: 'login',
1738
1741
  conta: 'account',
1742
+ plano: 'planos', assinar: 'planos', assinatura: 'planos', precos: 'planos',
1739
1743
  sair: 'exit', // sair é SAIR; desconectar a conta é /logout
1740
1744
  // o histórico
1741
1745
  // /historico deixou de ser apelido do /chats: agora procura nos pedidos
@@ -2478,30 +2482,46 @@ async function handleSlash(text) {
2478
2482
  console.log(c.muted(' A janela saiu na 10.0 — o PrimoCode agora é só o terminal.'));
2479
2483
  break;
2480
2484
 
2481
- /* A conta do Conecta Primo, sem navegador: e-mail e senha aqui
2482
- mesmo. O login inteiro mora em lib/conta.js; aqui é a tela dele. */
2485
+ /* A conta do PrimoCode (lib/conta.js). Padrão: abre o navegador, onde
2486
+ se cria a conta, entra ou redefine a senha. `/login senha` é o
2487
+ caminho sem navegador (SSH, servidor). */
2483
2488
  case 'login': {
2489
+ const partes = String(rest || '').trim().split(/\s+/);
2484
2490
  const jaEsta = conta.atual();
2485
- if (jaEsta && rest !== 'trocar') {
2491
+ if (jaEsta && !partes.includes('trocar')) {
2486
2492
  console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.email || (jaEsta.uid.slice(0, 12) + '…'))
2487
2493
  + c.muted(` · plano ${jaEsta.plano}`));
2488
2494
  console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/login trocar'));
2489
2495
  break;
2490
2496
  }
2491
- if (rest === 'trocar') conta.sair();
2492
-
2493
- const email = String(await ask(c.brand(' E-mail: ')) || '').trim();
2494
- if (!email) { console.log(c.muted(' Login cancelado.')); break; }
2495
- const senha = await askSecret(c.brand(' Senha: '));
2496
- if (!senha) { console.log(c.muted(' Login cancelado.')); break; }
2497
-
2498
- const sp = new Spinner('entrando').start();
2499
- const r = await conta.entrar({ email, senha });
2500
- sp.stop();
2497
+ if (jaEsta || conta.doConectaAntigo()) conta.sair();
2498
+
2499
+ let r;
2500
+ if (partes.includes('senha')) {
2501
+ const email = String(await ask(c.brand(' E-mail: ')) || '').trim();
2502
+ if (!email) { console.log(c.muted(' Login cancelado.')); break; }
2503
+ const senha = await askSecret(c.brand(' Senha: '));
2504
+ if (!senha) { console.log(c.muted(' Login cancelado.')); break; }
2505
+ const sp = new Spinner('entrando').start();
2506
+ r = await conta.entrar({ email, senha });
2507
+ sp.stop();
2508
+ } else {
2509
+ const sp = new Spinner('esperando você entrar no navegador').start();
2510
+ r = await conta.entrarPeloNavegador({
2511
+ aviso: (url) => {
2512
+ sp.stop();
2513
+ console.log(c.muted(' Abri o navegador para você criar a conta ou entrar.'));
2514
+ console.log(c.dim(' Não abriu? Copie: ') + c.brand(url));
2515
+ console.log(c.dim(' Sem navegador aqui? ') + c.brand('/login senha'));
2516
+ sp.start();
2517
+ },
2518
+ });
2519
+ sp.stop();
2520
+ }
2501
2521
  if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
2502
2522
  console.log(c.ok(' Conectado.')
2503
- + c.muted(` Plano ${r.plano}${r.pro ? '' : ' — assine Premium no app para liberar tudo'}`));
2504
- break;
2523
+ + c.muted(r.pro ? ` Plano ${r.plano}.` : ' Sua conta ainda não tem plano: ') + (r.pro ? '' : c.brand('/planos')));
2524
+ return r;
2505
2525
  }
2506
2526
 
2507
2527
  /* O histórico da conta — o MESMO que o app desktop e o celular
@@ -2558,29 +2578,28 @@ async function handleSlash(text) {
2558
2578
  break;
2559
2579
  }
2560
2580
  const sp = new Spinner('conferindo').start();
2561
- // O plano guardado é a foto do dia do login: quem assinou ontem
2562
- // continuaria gratuito. Quem sabe é o servidor.
2581
+ // O plano guardado é a foto do último login: quem sabe é o documento da conta.
2563
2582
  const s = await conta.sessao();
2564
2583
  sp.stop();
2565
- console.log(c.white(' Conta ') + c.muted(eu.uid));
2584
+ console.log(c.white(' Conta ') + c.muted(eu.email || eu.uid));
2566
2585
  if (s.ok) {
2567
- console.log(c.white(' Plano ')
2568
- + (s.pro ? c.brand(s.plano) : c.muted(s.plano + ' (gratuito)')));
2586
+ const ate = s.planoAte ? c.dim(` até ${new Date(s.planoAte).toLocaleDateString('pt-BR')}`) : '';
2587
+ console.log(c.white(' Plano ') + (s.pro ? c.brand(s.plano) + ate : c.muted('sem plano ') + c.brand('/planos')));
2569
2588
  } else if (s.offline) {
2570
2589
  console.log(c.white(' Plano ') + c.muted(eu.plano) + c.dim(' (sem rede — último conhecido)'));
2571
2590
  } else {
2572
2591
  console.log(c.err(` ${s.error}`));
2573
- break;
2574
- }
2575
- const d = await conta.dispositivos();
2576
- if (d.ok && d.dispositivos.length) {
2577
- console.log(c.white('\n Computadores conectados'));
2578
- for (const x of d.dispositivos) {
2579
- const quando = x.ultimo_uso
2580
- ? new Date(x.ultimo_uso * 1000).toLocaleDateString('pt-BR') : '—';
2581
- console.log(' ' + c.muted((x.apelido || 'Computador').padEnd(14)) + c.dim(quando));
2582
- }
2583
2592
  }
2593
+ console.log(c.dim(' Senha: ') + c.brand(`${conta.SITE}/entrar#esqueci`));
2594
+ break;
2595
+ }
2596
+
2597
+ case 'planos': {
2598
+ const sp = new Spinner('buscando os planos').start();
2599
+ const r = await planosTela.buscarPlanos(state.config.server);
2600
+ sp.stop();
2601
+ const eu = conta.atual();
2602
+ for (const l of planosTela.desenhar(r, { cores: c, atual: eu && eu.pro ? eu.plano : null, site: conta.SITE })) console.log(l);
2584
2603
  break;
2585
2604
  }
2586
2605
 
@@ -2771,6 +2790,8 @@ async function main() {
2771
2790
  process.exit(await require('../lib/mcp.js').main(process.argv.slice(3)));
2772
2791
  }
2773
2792
  cfg.ensureStructure();
2793
+ // O token da conta dura uma hora: renova agora e a cada minuto (lib/conta.js).
2794
+ await conta.manterVivo();
2774
2795
 
2775
2796
  // Padrão fixo: ~/primocode, não importa de onde o comando foi dado. Antes ele
2776
2797
  // adotava a pasta atual do terminal, e o mesmo "crie um site" caía num lugar
@@ -3047,7 +3068,7 @@ async function main() {
3047
3068
  cores: c,
3048
3069
  interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
3049
3070
  conectado: () => conta.atual(),
3050
- conectar: () => handleSlash('/login').then(() => conta.atual()),
3071
+ conectar: () => handleSlash('/login').then((r) => (r && r.ok === false ? r : conta.atual() || r)),
3051
3072
  sessao: () => conta.sessao(),
3052
3073
  }).then((r) => {
3053
3074
  // O bloqueio de quem não é Premium/Super. É de CORTESIA: este arquivo
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * boas-vindas.js — a abertura do PrimoCode, na primeira vez e em toda vez.
3
3
  *
4
- * Duas coisas acontecem aqui, nesta ordem: a pessoa entra na conta do Conecta
5
- * Primo AI, e o plano dela é conferido. Acabou. Não há terceira pergunta.
4
+ * Duas coisas acontecem aqui, nesta ordem: a pessoa entra na conta do
5
+ * PrimoCode, e o plano dela é conferido. Acabou. Não há terceira pergunta.
6
6
  *
7
7
  * ── POR QUE NÃO SE ESCOLHE MAIS A IA ─────────────────────────────────────
8
8
  * Existiam duas telas depois do login: "Com qual IA você quer trabalhar?" e
@@ -17,8 +17,9 @@
17
17
  * A API vem configurada no produto. Entrou na conta, pode pedir a primeira
18
18
  * coisa.
19
19
  *
20
- * Desde 23/09/2026 o login não abre mais o navegador: é e-mail e senha no
21
- * próprio terminal (/login, ver lib/conta.js).
20
+ * Desde 12/10/2026 a conta é do próprio PrimoCode (Firebase, sem o Conecta):
21
+ * o /login abre o navegador, onde se cria a conta, entra ou redefine a senha,
22
+ * e `/login senha` faz pelo terminal (ver lib/conta.js).
22
23
  *
23
24
  * ── O BLOQUEIO É DE CORTESIA ─────────────────────────────────────────────
24
25
  * Este arquivo vai publicado no npm com o código à vista: um `if (pro)` aqui
@@ -62,8 +63,8 @@ function esquecer() {
62
63
  * @param {object} op.cores
63
64
  * @param {boolean} op.interativo false = pipe/CI: não pergunta nada
64
65
  * @param {()=>any} op.conectado a credencial local, ou null
65
- * @param {()=>Promise<any>} op.conectar roda o login (e-mail e senha no terminal)
66
- * @param {()=>Promise<any>} op.sessao confirma o plano NO SERVIDOR
66
+ * @param {()=>Promise<any>} op.conectar roda o login (abre o navegador)
67
+ * @param {()=>Promise<any>} op.sessao confirma o plano no documento da conta
67
68
  */
68
69
  async function rodar({ perguntar, mostrar, cores: c, interativo = true,
69
70
  conectado, conectar, sessao } = {}) {
@@ -80,7 +81,7 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
80
81
  if (!credencial) {
81
82
  mostrar('');
82
83
  mostrar(c.bold(c.brand(' Bem-vindo ao PrimoCode.')));
83
- mostrar(c.muted(' Primeiro, entre na sua conta do Conecta Primo AI.'));
84
+ mostrar(c.muted(' Primeiro, crie sua conta ou entre nela: vou abrir o navegador.'));
84
85
  mostrar('');
85
86
  const r = await conectar();
86
87
  if (!r || r.ok === false) {
@@ -112,13 +113,11 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
112
113
  const pro = s && s.ok ? !!s.pro : !!(credencial && credencial.pro);
113
114
  if (!pro) {
114
115
  mostrar('');
115
- mostrar(c.err(' Você ainda não tem acesso ao PrimoCode.'));
116
- mostrar(c.muted(` Sua conta está no plano ${plano}, e o PrimoCode pede `)
117
- + c.brand('Premium') + c.muted(' ou ') + c.brand('Super') + c.muted('.'));
116
+ mostrar(c.err(' Sua conta ainda não tem um plano.'));
117
+ mostrar(c.muted(' O PrimoCode é por assinatura, a partir de ') + c.brand('R$ 69,90/mês') + c.muted('.'));
118
118
  mostrar('');
119
- mostrar(c.dim(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
120
- + c.dim(' e rode ') + c.brand('primo') + c.dim(' de novo.'));
121
- mostrar(c.dim(' Já assinou? ') + c.brand('/login')
119
+ mostrar(c.dim(' Veja os planos e assine: ') + c.brand('/planos'));
120
+ mostrar(c.dim(' Já assinou? ') + c.brand('/account')
122
121
  + c.dim(' atualiza o plano nesta máquina.'));
123
122
  mostrar('');
124
123
  return { liberado: false, motivo: 'sem-plano', plano };
package/lib/config.js CHANGED
@@ -32,7 +32,8 @@ const PROJETO_ATIVO_FILE = path.join(CONFIG_DIR, 'projeto-ativo.json');
32
32
  * função no meio de um turno. Ler quatro linhas de JSON é mais barato que
33
33
  * essa aula. */
34
34
  function tokenDaConta() {
35
- try { return JSON.parse(fs.readFileSync(CONTA_FILE, 'utf8')).token || null; }
35
+ // O token de acesso do Firebase (dura 1h; o conta.manterVivo renova no arquivo).
36
+ try { return JSON.parse(fs.readFileSync(CONTA_FILE, 'utf8')).idToken || null; }
36
37
  catch { return null; }
37
38
  }
38
39
 
@@ -321,7 +322,9 @@ function loadConfig() {
321
322
  * arquivo, e o arquivo ganha da conta. Quem hospeda o próprio
322
323
  * servidor e configurou um token não pode ter isso trocado por
323
324
  * entrar numa conta. */
324
- token: process.env.PRIMOCODE_TOKEN || file.token || tokenDaConta(),
325
+ /* Getter, e não valor: o token da conta vence em uma hora e é
326
+ renovado no arquivo; uma sessão de três horas leria o vencido. */
327
+ get token() { return process.env.PRIMOCODE_TOKEN || file.token || tokenDaConta(); },
325
328
  autoApply: file.autoApply === true,
326
329
  currentProject: file.currentProject || null,
327
330
  userName: file.userName || null,