primocode 8.33.1 → 8.34.1

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/bin/primocode.js CHANGED
@@ -63,6 +63,12 @@ const state = {
63
63
  // O histórico que o app desktop e o celular enxergam. Cifrado antes de
64
64
  // sair daqui; o porquê de cada peça está em lib/nuvem.js.
65
65
  gravador: nuvem.gravador(),
66
+ /* Começa LIBERADO e a abertura decide depois. Começar bloqueado deixaria
67
+ um instante em que a pessoa digita e leva "sem acesso" antes de o
68
+ plano ter sido conferido — e num pipe, onde a abertura nem roda, o
69
+ bloqueio ficaria para sempre. */
70
+ liberado: true,
71
+ motivoBloqueio: null,
66
72
  lastResponse: '',
67
73
  currentProject: null,
68
74
  workDir: null, // pasta onde o agente trabalha nesta sessão
@@ -80,7 +86,6 @@ ${c.bold('Comandos')}
80
86
  ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
81
87
  ${c.brand('/mode')} /mode primocode|chat|auto
82
88
  ${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
83
- ${c.brand('/claude')} liga o Claude Code local como motor (sem API paga) · /claude off volta ao padrão
84
89
  ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
85
90
  ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
86
91
  ${c.brand('/tudo')} lista tudo que ele sabe fazer
@@ -90,7 +95,7 @@ ${c.bold('Comandos')}
90
95
  ${c.brand('/open')} <arquivo|url> abre no navegador
91
96
  ${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
92
97
  ${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
93
- ${c.brand('/codex')} usa o Codex da SUA conta como motor (modelo barato)
98
+ ${c.brand('/gpt')} usa o GPT (Codex) da SUA conta como motor
94
99
  ${c.brand('/chave')} <provedor> sua própria chave de API (Claude, Codex)
95
100
  ${c.brand('/conectar')} entra na sua conta Conecta Primo · ${c.brand('/conta')} mostra o plano
96
101
  ${c.brand('/conversas')} [n] suas conversas salvas — as mesmas do app e do celular
@@ -126,7 +131,6 @@ const SLASH_COMANDOS = [
126
131
  ['/dir', 'trabalhar numa pasta sua'],
127
132
  ['/mode', 'primocode | chat | auto'],
128
133
  ['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
129
- ['/claude', 'liga o Claude Code local como motor · /claude off volta ao padrão'],
130
134
  ['/attach', 'anexa arquivo ao pedido'],
131
135
  ['/detach', 'remove os anexos'],
132
136
  ['/files', 'lista os anexos'],
@@ -137,7 +141,7 @@ const SLASH_COMANDOS = [
137
141
  ['/open', 'abre no navegador'],
138
142
  ['/rc', 'abre a janela do PrimoCode'],
139
143
  ['/claude', 'usa o Claude Code da sua conta como motor'],
140
- ['/codex', 'usa o Codex da sua conta como motor'],
144
+ ['/gpt', 'usa o GPT (Codex) da sua conta como motor'],
141
145
  ['/chave', 'sua própria chave de API (Claude, Codex)'],
142
146
  ['/conectar', 'entra na sua conta Conecta Primo'],
143
147
  ['/conta', 'sua conta, plano e computadores conectados'],
@@ -264,9 +268,31 @@ function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
264
268
  // — não um novo comando. Sem isso o "s" de confirmação virava prompt para a IA.
265
269
  let pendingAsk = null;
266
270
 
271
+ /* ── O QUE A PESSOA ADIANTA ──────────────────────────────────────────────
272
+ * Enquanto a abertura corre — e ela demora, porque confere o plano no
273
+ * servidor — o que for digitado não é comando nem resposta ainda: a pergunta
274
+ * ainda não apareceu. Fica aqui.
275
+ *
276
+ * Sem isto, esse texto caía como PEDIDO no agente. Medido no pacote
277
+ * publicado: o "2" da escolha da IA virou pergunta no chat e o modelo
278
+ * respondeu "Oi! Como posso ajudar?".
279
+ *
280
+ * E guardar não bastava: preso na fila de comandos, ele nunca chegava à
281
+ * pergunta que veio depois. Por isso o `ask` olha aqui ANTES de esperar uma
282
+ * linha nova — quem digitou 2 antes de ver o menu escolheu a 2, e a escolha
283
+ * dela vale.
284
+ */
285
+ let segurando = false;
286
+ const adiantadas = [];
287
+
267
288
  function ask(question) {
268
289
  return new Promise((resolve) => {
269
- if (rl) { process.stdout.write(question); pendingAsk = resolve; return; }
290
+ if (rl) {
291
+ process.stdout.write(question);
292
+ if (adiantadas.length) { const a = adiantadas.shift(); process.stdout.write(a + '\n'); return resolve(a); }
293
+ pendingAsk = resolve;
294
+ return;
295
+ }
270
296
  const tmp = readline.createInterface({ input: process.stdin, output: process.stdout });
271
297
  tmp.question(question, (a) => { tmp.close(); resolve(a); });
272
298
  });
@@ -420,6 +446,22 @@ function prepararPasta(userText) {
420
446
  }
421
447
 
422
448
  async function sendPrompt(userText) {
449
+ /* O portão do plano. Sem ele o bloqueio da abertura seria só um texto na
450
+ tela: a pessoa leria "você não tem acesso" e mandaria a primeira coisa
451
+ do mesmo jeito, e funcionaria.
452
+
453
+ De cortesia, não de segurança — este arquivo vai publicado no npm com o
454
+ código à vista, e a checagem é uma linha que qualquer um apaga. Quem
455
+ segura de verdade é o primocode-server. O que ele faz aqui é dizer o
456
+ que houve e como resolver, em vez de deixar a pessoa achando que o
457
+ produto está quebrado. */
458
+ if (state.liberado === false) {
459
+ console.log(c.err(' O PrimoCode pede o plano Premium ou Super.'));
460
+ console.log(c.muted(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
461
+ + c.muted(' · já assinou? ') + c.brand('/conectar'));
462
+ return;
463
+ }
464
+
423
465
  let prompt = userText;
424
466
  if (state.attachedFiles.length) {
425
467
  const parts = state.attachedFiles.map((f) => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
@@ -821,7 +863,10 @@ async function handleSlash(text) {
821
863
 
822
864
  /* O Codex da conta da pessoa, com o modelo barato.
823
865
  O protocolo é o MESMO do /claude — ver lib/codex-engine.js. */
824
- case 'codex': {
866
+ /* `/gpt` e `/codex` são a mesma coisa e existem os dois de propósito:
867
+ o programa se chama Codex, mas quem usa fala "GPT" — e o comando que
868
+ a pessoa tenta é o nome que ela tem na cabeça, não o do fabricante. */
869
+ case 'gpt': case 'codex': {
825
870
  if (rest.toLowerCase() === 'off') {
826
871
  if (state.engine !== 'codex') { console.log(c.muted(' Já estava no motor padrão.')); break; }
827
872
  state.engine = 'server';
@@ -1504,40 +1549,56 @@ async function main() {
1504
1549
 
1505
1550
  rl.on('line', (line) => {
1506
1551
  if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
1552
+ // Abertura correndo: guarda em vez de mandar ao agente.
1553
+ if (segurando) { adiantadas.push(line); return; }
1507
1554
  queue.push(line);
1508
1555
  drain();
1509
1556
  });
1510
1557
 
1511
1558
  rl.on('close', () => { closed = true; if (!busy) sayBye(); });
1512
1559
 
1513
- /* A primeira vez — e ela roda AQUI, depois de `line` e `close` existirem.
1560
+ /* A ABERTURA — e ela roda AQUI, depois de `line` e `close` existirem.
1514
1561
  *
1515
- * Estava lá em cima, logo depois de criar o readline. Parecia certo e
1516
- * estava errado: o `await` do assistente segurava o resto do main(), e
1517
- * tudo o que chegasse pelo teclado nesse meio-tempo caía no vazio, porque
1518
- * ainda não havia ouvinte de `line` para pegar. Quem colasse dois comandos
1519
- * de uma vez perdia o segundo, e quem fechasse a entrada (Ctrl+D, um
1520
- * script) saía sem nem o "Até logo" — porque o ouvinte de `close` também
1521
- * ainda não existia.
1562
+ * Estava antes deles. O `await` segurava o resto do main(), e tudo o que
1563
+ * chegasse pelo teclado nesse meio-tempo caía no vazio: não havia ouvinte
1564
+ * para pegar. Quem colasse dois comandos perdia o segundo, e quem
1565
+ * fechasse a entrada saía sem nem o "Até logo". Digitando devagar ninguém
1566
+ * via — os ouvintes ficam prontos milissegundos depois. */
1567
+ /* A FILA FICA SEGURA ENQUANTO A ABERTURA CORRE.
1522
1568
  *
1523
- * Digitando devagar ninguém via: os ouvintes ficavam prontos milissegundos
1524
- * depois. Foi preciso um terminal de mentira mandando as linhas de uma vez
1525
- * para o defeito aparecer.
1569
+ * `busy` já é o freio do `drain`, e é ele que se usa aqui. Sem isso, tudo
1570
+ * o que a pessoa digitar antes de a pergunta aparecer — e ela demora,
1571
+ * porque confere o plano no servidor — cai como PEDIDO no agente. Medido
1572
+ * no pacote publicado: o "2" da escolha da IA virou uma pergunta no chat,
1573
+ * e o modelo respondeu "Oi! Como posso ajudar?".
1526
1574
  *
1527
- * Aparece UMA vez; depois disso, /conectar e /chave é que são o caminho. */
1528
- if (!boasVindas.jaAbriu({ arquivoConfig: cfg.CONFIG_FILE })) {
1529
- boasVindas.rodar({
1530
- perguntar: ask,
1531
- mostrar: (t) => console.log(t),
1532
- cores: c,
1533
- interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
1534
- conectar: () => handleSlash('/conectar'),
1535
- configurarChave: () => handleSlash('/chave'),
1536
- }).then(() => { if (!closed && !busy) rl.prompt(); })
1537
- // Um assistente que falha não pode levar o CLI junto: ele é uma
1538
- // apresentação, não uma condição para usar o programa.
1539
- .catch(() => {});
1540
- }
1575
+ * O ouvinte de `line` continua atendendo o `ask` normalmente: ele olha o
1576
+ * `pendingAsk` antes da fila. O que fica retido é só o que não era
1577
+ * resposta a nada. */
1578
+ segurando = true;
1579
+ boasVindas.rodar({
1580
+ perguntar: ask,
1581
+ mostrar: (t) => console.log(t),
1582
+ cores: c,
1583
+ interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
1584
+ conectado: () => conta.atual(),
1585
+ conectar: () => handleSlash('/conectar').then(() => conta.atual()),
1586
+ sessao: () => conta.sessao(),
1587
+ aplicar: (cmd) => handleSlash(cmd),
1588
+ }).then((r) => {
1589
+ // O bloqueio de quem não é Premium/Super. É de CORTESIA: este arquivo
1590
+ // vai publicado no npm com o código à vista. Quem segura de verdade é
1591
+ // o primocode-server, que tem a chave do provedor.
1592
+ state.liberado = !r || r.liberado !== false;
1593
+ state.motivoBloqueio = r && r.motivo;
1594
+ }).catch(() => { state.liberado = true; }).then(() => {
1595
+ // O que sobrou de adiantado não era resposta de nada: vira comando,
1596
+ // na ordem em que foi digitado. Nada se perde, só esperou a vez.
1597
+ segurando = false;
1598
+ while (adiantadas.length) queue.push(adiantadas.shift());
1599
+ if (queue.length) drain();
1600
+ else if (!closed) rl.prompt();
1601
+ });
1541
1602
  }
1542
1603
 
1543
1604
  main().catch((e) => { console.error(c.err(e.message || e)); process.exit(1); });
@@ -1,26 +1,40 @@
1
1
  /**
2
- * boas-vindas.js — a primeira vez que alguém roda `primo`.
2
+ * boas-vindas.js — a abertura do PrimoCode.
3
3
  *
4
- * "Eu desinstalei e instalei novamente, está na versão nova, e quando mandei
5
- * ele já abriu. Não veio a parte pra mim fazer login, selecionar qual IA eu
6
- * quero — se quero usar já o Conecta Primo AI como IA, se quero usar outra
7
- * IA. Não tem nada disso."
4
+ * "O usuário vai rodar o comando de instalação do npm, vai instalar, vai
5
+ * rodar `primo`, e aí, como acabou de instalar, vai aparecer pra fazer
6
+ * login. Ele vai abrir automaticamente a janela lá no Conecta Primo AI com
7
+ * o token, tudo pra fazer login na conta. Aí fez o login, o terminal checa
8
+ * se sua conta é Premium ou Super. Se for, ele libera para a próxima etapa.
9
+ * Se não, ele fala que você não tem acesso ao PrimoCode, porque tem que ter
10
+ * o plano Premium ou Super.
8
11
  *
9
- * Tinha razão: o CLI abria direto no prompt. Quem instalava não descobria que
10
- * existe conta, não descobria que dá para usar a própria chave, e ia usar o
11
- * gratuito achando que era só aquilo — até bater na cota e concluir que o
12
- * produto não presta.
12
+ * A próxima etapa, se liberar o acesso, é selecionar: continuar com a API
13
+ * padrão, ou usar conta Claude, GPT etc. Continuar com a API padrão, ele
14
+ * libera e aparece aquela tela pronta pra enviar o primeiro pedido. Se
15
+ * clicar em continuar com Claude/GPT, você seleciona, deu Enter, ele vai
16
+ * pra próxima tela, e aí você seleciona Claude ou GPT — e quando seleciona,
17
+ * ele dá aquele /gpt ou /claude, e vai funcionar como já funciona hoje."
13
18
  *
14
- * ── POR QUE ISTO É UM MÓDULO, E NÃO CÓDIGO SOLTO NO bin/ ─────────────────
15
- * Porque tem decisão dentro: quando aparecer, o que perguntar, o que gravar,
16
- * e o que fazer quando a pessoa só aperta Enter. Isso precisa de teste, e
17
- * teste não digita em terminal. Aqui a entrada e a saída são funções que o
18
- * teste passa; o bin/ liga nas de verdade.
19
+ * ── A ORDEM É O PRODUTO ──────────────────────────────────────────────────
20
+ * Conectar → conferir o plano → escolher o motor. Nesta ordem, e não em
21
+ * outra. A primeira versão disto oferecia o login como UMA OPÇÃO de menu, ao
22
+ * lado de "decidir depois", e deixava quem não assina usar assim mesmo. Era
23
+ * um produto diferente do que foi pedido.
19
24
  *
20
- * ── APARECE UMA VEZ SÓ, E DÁ PARA PULAR ──────────────────────────────────
21
- * Um assistente que volta toda vez vira obstáculo. Este grava a marca de que
22
- * já rodou e não volta — nem se a pessoa escolher "depois". Quem quiser
23
- * rever chama `/conectar` ou `/chave`, que é onde as duas coisas moram.
25
+ * ── O BLOQUEIO É DELIBERADO ──────────────────────────────────────────────
26
+ * Sem Premium ou Super, o PrimoCode não trabalha. Isso é uma decisão de
27
+ * produto do dono, tomada com o custo na mesa: quem já instalou no gratuito
28
+ * perde o acesso na atualização. Está escrito aqui para ninguém "consertar"
29
+ * isso mais adiante achando que é bug.
30
+ *
31
+ * O bloqueio é de CORTESIA, não de segurança: este arquivo vai publicado no
32
+ * npm com o código à vista e qualquer um apaga a checagem. Quem segura de
33
+ * verdade é o primocode-server, que é quem tem a chave do provedor.
34
+ *
35
+ * ── POR QUE ISTO É UM MÓDULO ─────────────────────────────────────────────
36
+ * Porque tem decisão dentro, e decisão precisa de teste — e teste não digita
37
+ * em terminal. Aqui a entrada e a saída são funções que quem chama fornece.
24
38
  */
25
39
 
26
40
  'use strict';
@@ -31,131 +45,201 @@ const path = require('path');
31
45
 
32
46
  const MARCA = () => path.join(os.homedir(), '.primocode', 'ja-abriu.json');
33
47
 
34
- /**
35
- * Já passou por aqui?
36
- *
37
- * Duas provas, e basta uma: a marca própria, ou um config.json que já existe
38
- * de uma versão anterior. A segunda evita que quem já usava o PrimoCode há
39
- * meses veja um "bem-vindo, é sua primeira vez" ao atualizar — o que faria a
40
- * atualização parecer uma reinstalação, e é o tipo de coisa que assusta.
41
- */
42
- function jaAbriu({ arquivoConfig } = {}) {
43
- try { if (fs.existsSync(MARCA())) return true; } catch {}
44
- try { if (arquivoConfig && fs.existsSync(arquivoConfig)) return true; } catch {}
45
- return false;
48
+ /** Já escolheu o motor alguma vez? A marca guarda a escolha, não o login. */
49
+ function jaEscolheu() {
50
+ try { return fs.existsSync(MARCA()); } catch { return false; }
46
51
  }
47
52
 
48
- function marcarQueAbriu(dados = {}) {
53
+ function marcar(dados = {}) {
49
54
  try {
50
55
  const caminho = MARCA();
51
56
  fs.mkdirSync(path.dirname(caminho), { recursive: true });
52
- fs.writeFileSync(caminho, JSON.stringify({
53
- em: new Date().toISOString(), ...dados,
54
- }, null, 2), 'utf8');
57
+ fs.writeFileSync(caminho, JSON.stringify({ em: new Date().toISOString(), ...dados }, null, 2), 'utf8');
55
58
  return true;
56
59
  } catch { return false; }
57
60
  }
58
61
 
59
- /** Para o teste e para quem quiser rever: apaga a marca. */
60
62
  function esquecer() {
61
63
  try { fs.rmSync(MARCA(), { force: true }); return true; } catch { return false; }
62
64
  }
63
65
 
64
- const ESCOLHAS = [
65
- {
66
- id: 'conecta',
67
- titulo: 'Usar a IA do Conecta Primo AI',
68
- detalhe: 'A que já vem pronta. Entra na sua conta; Premium ou Super libera o limite.',
69
- },
70
- {
71
- id: 'propria',
72
- titulo: 'Usar a minha própria IA',
73
- detalhe: 'Sua chave do Claude ou do Codex, ou o Claude Code / Codex já instalados.',
74
- },
75
- {
76
- id: 'depois',
77
- titulo: 'Decidir depois',
78
- detalhe: 'Começa no gratuito. /conectar e /chave estão lá quando você quiser.',
79
- },
66
+ // ── as duas telas de escolha ─────────────────────────────────────────────
67
+
68
+ const MOTORES = [
69
+ { id: 'padrao', titulo: 'Continuar com a API padrão',
70
+ detalhe: 'A que já vem pronta, incluída no seu plano.' },
71
+ { id: 'propria', titulo: 'Usar a minha conta (Claude, GPT…)',
72
+ detalhe: 'O Claude Code ou o Codex que você já usa, na sua assinatura.' },
80
73
  ];
81
74
 
82
- /** O que a pessoa digitou → qual escolha. Aceita número, o id, ou Enter. */
83
- function lerEscolha(texto) {
84
- const t = String(texto || '').trim().toLowerCase();
85
- // Enter puro é a primeira opção: é a recomendada, e quem aperta Enter sem
86
- // ler está dizendo "faz o que for melhor", não "não quero nada".
87
- if (!t) return ESCOLHAS[0].id;
88
- const n = Number(t);
89
- if (Number.isInteger(n) && n >= 1 && n <= ESCOLHAS.length) return ESCOLHAS[n - 1].id;
90
- for (const e of ESCOLHAS) if (t === e.id || t.startsWith(e.id.slice(0, 4))) return e.id;
91
- if (/^(n|nao|não|depois|pular|skip)$/.test(t)) return 'depois';
92
- if (/claude|codex|chave|propri/.test(t)) return 'propria';
93
- if (/conecta|primo|conta|entrar/.test(t)) return 'conecta';
94
- return null; // não entendi: quem chama pergunta de novo
95
- }
75
+ const PROVEDORES = [
76
+ { id: 'claude', titulo: 'Claude', comando: '/claude',
77
+ detalhe: 'Usa o Claude Code da sua conta, no Opus.' },
78
+ { id: 'gpt', titulo: 'GPT', comando: '/gpt',
79
+ detalhe: 'Usa o Codex da sua conta, num modelo barato.' },
80
+ ];
96
81
 
97
82
  /**
98
- * O assistente.
83
+ * O que a pessoa digitou → qual item da lista.
99
84
  *
100
- * @param {object} op
101
- * @param {(q:string)=>Promise<string>} op.perguntar lê uma linha
102
- * @param {(s:string)=>void} op.mostrar escreve na tela
103
- * @param {object} op.cores o `c` do ui.js
104
- * @param {()=>Promise<any>} op.conectar roda o login (o /conectar)
105
- * @param {()=>Promise<any>} op.configurarChave abre o /chave
106
- * @param {boolean} op.interativo false = não pergunta nada
85
+ * Aceita o número, o id, o começo do nome, e Enter (que é sempre o primeiro:
86
+ * quem aperta Enter sem ler está dizendo "faz o que for melhor").
107
87
  */
108
- async function rodar({ perguntar, mostrar, cores: c, conectar, configurarChave,
109
- interativo = true } = {}) {
110
- /* Sem terminal de verdade (pipe, script, CI) NÃO se pergunta nada. Um
111
- assistente que espera resposta num lugar onde ninguém pode responder
112
- trava o processo para sempre — e trava calado, que é o pior jeito. */
113
- if (!interativo) {
114
- marcarQueAbriu({ escolha: 'sem-terminal' });
115
- return { ok: true, escolha: 'depois', perguntou: false };
88
+ function escolher(texto, lista) {
89
+ const t = String(texto || '').trim().toLowerCase();
90
+ if (!t) return lista[0].id;
91
+ const n = Number(t);
92
+ if (Number.isInteger(n) && n >= 1 && n <= lista.length) return lista[n - 1].id;
93
+ for (const e of lista) {
94
+ if (t === e.id || t === e.titulo.toLowerCase()) return e.id;
95
+ if (e.id.length >= 3 && t.startsWith(e.id.slice(0, 3))) return e.id;
96
+ }
97
+ // O jeito como as pessoas falam, e não como a lista se chama.
98
+ if (lista === MOTORES) {
99
+ if (/padr|inclu|mesmo|pronta|voc[eê]s|de voc/.test(t)) return 'padrao';
100
+ if (/minha|meu|pr[oó]pri|conta|chave|assinatura/.test(t)) return 'propria';
101
+ } else {
102
+ if (/claude|anthropic|opus|sonnet/.test(t)) return 'claude';
103
+ if (/gpt|openai|codex|chatgpt/.test(t)) return 'gpt';
116
104
  }
105
+ return null;
106
+ }
117
107
 
108
+ async function perguntarDaLista({ perguntar, mostrar, cores: c, titulo, ajuda, lista }) {
118
109
  mostrar('');
119
- mostrar(c.bold(c.brand(' Primeira vez por aqui.')));
120
- mostrar(c.muted(' Escolha qual IA vai pensar por trás do PrimoCode:'));
110
+ mostrar(c.bold(c.brand(' ' + titulo)));
111
+ if (ajuda) mostrar(c.muted(' ' + ajuda));
121
112
  mostrar('');
122
- ESCOLHAS.forEach((e, i) => {
123
- mostrar(' ' + c.brand(String(i + 1)) + c.muted('. ') + c.white(e.titulo)
124
- + (i === 0 ? c.dim(' (recomendado)') : ''));
113
+ lista.forEach((e, i) => {
114
+ mostrar(' ' + c.brand(String(i + 1)) + c.muted('. ') + c.white(e.titulo));
125
115
  mostrar(' ' + c.dim(e.detalhe));
126
116
  });
127
117
  mostrar('');
128
118
 
129
- let escolha = null;
130
- for (let tentativa = 0; tentativa < 3 && !escolha; tentativa++) {
131
- const dito = await perguntar(c.brand(' ❯ ') + c.muted('1, 2 ou 3 (Enter = 1) '));
132
- escolha = lerEscolha(dito);
133
- if (!escolha) mostrar(c.muted(' Não entendi. Digite 1, 2 ou 3.'));
119
+ for (let tentativa = 0; tentativa < 3; tentativa++) {
120
+ const dito = await perguntar(c.brand(' ❯ ')
121
+ + c.muted(`1 a ${lista.length} (Enter = 1) `));
122
+ const qual = escolher(dito, lista);
123
+ if (qual) return qual;
124
+ mostrar(c.muted(` Não entendi. Digite 1 a ${lista.length}.`));
134
125
  }
135
- // Três tentativas sem entender: segue no gratuito em vez de insistir.
136
- // Ninguém deve ficar preso numa pergunta para usar o programa.
137
- if (!escolha) escolha = 'depois';
126
+ // Três tentativas sem entender: fica com a primeira em vez de prender
127
+ // alguém numa pergunta para usar o programa.
128
+ return lista[0].id;
129
+ }
130
+
131
+ // ── a abertura ───────────────────────────────────────────────────────────
132
+
133
+ /**
134
+ * @param {object} op
135
+ * @param {(q:string)=>Promise<string>} op.perguntar
136
+ * @param {(s:string)=>void} op.mostrar
137
+ * @param {object} op.cores
138
+ * @param {boolean} op.interativo false = pipe/CI: não pergunta nada
139
+ * @param {()=>any} op.conectado a credencial local, ou null
140
+ * @param {()=>Promise<any>} op.conectar roda o login (abre o navegador)
141
+ * @param {()=>Promise<any>} op.sessao confirma o plano NO SERVIDOR
142
+ * @param {(cmd:string)=>Promise<any>} op.aplicar executa /claude ou /gpt
143
+ */
144
+ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
145
+ conectado, conectar, sessao, aplicar } = {}) {
138
146
 
139
- let resultado = { escolha };
140
- if (escolha === 'conecta' && conectar) {
147
+ /* Sem terminal de verdade (pipe, script, CI) não se pergunta nada — uma
148
+ pergunta onde ninguém pode responder trava o processo para sempre, e
149
+ trava calado. Também não se BLOQUEIA: quem chamou não pode fazer login,
150
+ e barrar aqui transformaria um script quebrado num script mudo. Quem
151
+ segura de verdade é o servidor. */
152
+ if (!interativo) return { liberado: true, motivo: 'sem-terminal', perguntou: false };
153
+
154
+ // ── 1. conectar ──────────────────────────────────────────────────────
155
+ let credencial = conectado ? conectado() : null;
156
+ if (!credencial) {
157
+ mostrar('');
158
+ mostrar(c.bold(c.brand(' Bem-vindo ao PrimoCode.')));
159
+ mostrar(c.muted(' Primeiro, entre na sua conta do Conecta Primo AI.'));
160
+ mostrar(c.dim(' Vou abrir o navegador; é só confirmar por lá.'));
161
+ mostrar('');
162
+ const r = await conectar();
163
+ if (!r || r.ok === false) {
164
+ mostrar('');
165
+ mostrar(c.err(' Não deu para conectar' + (r && r.error ? `: ${r.error}` : '.')));
166
+ mostrar(c.muted(' Tente de novo com ') + c.brand('/conectar') + c.muted('.'));
167
+ return { liberado: false, motivo: 'sem-login' };
168
+ }
169
+ credencial = conectado ? conectado() : { plano: r.plano };
170
+ }
171
+
172
+ // ── 2. o plano ───────────────────────────────────────────────────────
173
+ const s = await sessao();
174
+ if (s && s.offline) {
175
+ /* Sem rede não se decide. Bloquear alguém porque o wi-fi tropeçou
176
+ seria transformar um problema de rede num "você não tem acesso" —
177
+ e a pessoa iria conferir a assinatura, que está em dia. Vale o
178
+ último plano conhecido; quem confere de verdade é o servidor, na
179
+ primeira chamada que sair daqui. */
180
+ mostrar(c.muted(' Sem rede para confirmar o plano — seguindo com o último conhecido.'));
181
+ } else if (!s || !s.ok) {
182
+ mostrar('');
183
+ mostrar(c.err(' Não consegui confirmar sua conta' + (s && s.error ? `: ${s.error}` : '.')));
184
+ mostrar(c.muted(' Entre de novo com ') + c.brand('/conectar') + c.muted('.'));
185
+ return { liberado: false, motivo: 'sessao-invalida' };
186
+ }
187
+
188
+ const plano = (s && s.ok ? s.plano : credencial && credencial.plano) || 'free';
189
+ const pro = s && s.ok ? !!s.pro : !!(credencial && credencial.pro);
190
+ if (!pro) {
191
+ mostrar('');
192
+ mostrar(c.err(' Você ainda não tem acesso ao PrimoCode.'));
193
+ mostrar(c.muted(` Sua conta está no plano ${plano}, e o PrimoCode pede `)
194
+ + c.brand('Premium') + c.muted(' ou ') + c.brand('Super') + c.muted('.'));
195
+ mostrar('');
196
+ mostrar(c.dim(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
197
+ + c.dim(' e rode ') + c.brand('primo') + c.dim(' de novo.'));
198
+ mostrar(c.dim(' Já assinou? ') + c.brand('/conectar')
199
+ + c.dim(' atualiza o plano nesta máquina.'));
141
200
  mostrar('');
142
- resultado.conexao = await conectar();
143
- } else if (escolha === 'propria' && configurarChave) {
201
+ return { liberado: false, motivo: 'sem-plano', plano };
202
+ }
203
+
204
+ mostrar('');
205
+ mostrar(c.ok(' Conta liberada.') + c.muted(` Plano ${plano}.`));
206
+
207
+ // ── 3. o motor ───────────────────────────────────────────────────────
208
+ // Só na primeira vez: quem já escolheu não quer responder isso toda vez.
209
+ if (jaEscolheu()) return { liberado: true, motivo: 'ja-escolheu', plano, perguntou: false };
210
+
211
+ const motor = await perguntarDaLista({
212
+ perguntar, mostrar, cores: c,
213
+ titulo: 'Com qual IA você quer trabalhar?',
214
+ lista: MOTORES,
215
+ });
216
+
217
+ if (motor === 'padrao') {
218
+ marcar({ motor: 'padrao', plano });
144
219
  mostrar('');
145
- resultado.chave = await configurarChave();
146
- } else if (escolha === 'depois') {
220
+ mostrar(c.muted(' Pronto. Pode pedir a primeira coisa.'));
147
221
  mostrar('');
148
- mostrar(c.muted(' Tudo bem. Você está no gratuito — ele funciona, mas tem cota.'));
149
- mostrar(c.dim(' ') + c.brand('/conectar') + c.dim(' entra na sua conta · ')
150
- + c.brand('/chave') + c.dim(' usa a sua própria IA'));
222
+ return { liberado: true, motor: 'padrao', plano, perguntou: true };
151
223
  }
152
224
 
153
- // A marca é gravada DEPOIS de tudo, mas grava mesmo se o login falhar:
154
- // insistir no assistente a cada abertura porque a rede caiu uma vez
155
- // seria punir a pessoa por um problema que não é dela.
156
- marcarQueAbriu({ escolha });
225
+ // ── 4. qual provedor ─────────────────────────────────────────────────
226
+ const qual = await perguntarDaLista({
227
+ perguntar, mostrar, cores: c,
228
+ titulo: 'Qual conta?',
229
+ ajuda: 'Ele usa o programa que você já tem instalado e logado.',
230
+ lista: PROVEDORES,
231
+ });
232
+ const escolhido = PROVEDORES.find((p) => p.id === qual) || PROVEDORES[0];
233
+
234
+ marcar({ motor: escolhido.id, plano });
235
+ mostrar('');
236
+ if (aplicar) await aplicar(escolhido.comando);
157
237
  mostrar('');
158
- return { ok: true, perguntou: true, ...resultado };
238
+ return { liberado: true, motor: escolhido.id, comando: escolhido.comando,
239
+ plano, perguntou: true };
159
240
  }
160
241
 
161
- module.exports = { jaAbriu, marcarQueAbriu, esquecer, lerEscolha, rodar, ESCOLHAS, MARCA };
242
+ module.exports = {
243
+ rodar, jaEscolheu, marcar, esquecer, escolher,
244
+ MOTORES, PROVEDORES, MARCA,
245
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.33.1",
4
- "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
3
+ "version": "8.34.1",
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": {
7
7
  "primocode": "bin/primocode.js"