primocode 8.31.0 → 8.32.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/bin/primocode.js CHANGED
@@ -21,6 +21,8 @@ const tools = require('../lib/tools');
21
21
  const cfg = require('../lib/config');
22
22
  const { runAct, interromper } = require('../lib/act');
23
23
  const claudeEngine = require('../lib/claude-engine');
24
+ const codexEngine = require('../lib/codex-engine');
25
+ const chaves = require('../lib/chaves');
24
26
  const pasta = require('../lib/pasta');
25
27
  const catalogo = require('../lib/catalogo');
26
28
  const effort = require('../lib/effort');
@@ -64,7 +66,7 @@ const state = {
64
66
  currentProject: null,
65
67
  workDir: null, // pasta onde o agente trabalha nesta sessão
66
68
  workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
67
- engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude)
69
+ engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude) | 'codex' (/codex)
68
70
  };
69
71
 
70
72
  // A janela do /app, quando aberta. Uma por sessão: o segundo /app traz a que
@@ -86,6 +88,9 @@ ${c.bold('Comandos')}
86
88
  ${c.brand('/run')} <comando> executa no shell do projeto
87
89
  ${c.brand('/open')} <arquivo|url> abre no navegador
88
90
  ${c.brand('/app')} abre a janela do PrimoCode (chat, sem terminal)
91
+ ${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
92
+ ${c.brand('/codex')} usa o Codex da SUA conta como motor (modelo barato)
93
+ ${c.brand('/chave')} <provedor> sua própria chave de API (Claude, Codex)
89
94
  ${c.brand('/entrar')} entra na sua conta Conecta Primo · ${c.brand('/conta')} mostra o plano
90
95
  ${c.brand('/conversas')} [n] suas conversas salvas — as mesmas do app e do celular
91
96
  ${c.brand('/sair')} desconecta este computador
@@ -130,6 +135,9 @@ const SLASH_COMANDOS = [
130
135
  ['/run', 'roda um comando'],
131
136
  ['/open', 'abre no navegador'],
132
137
  ['/app', 'abre a janela do PrimoCode'],
138
+ ['/claude', 'usa o Claude Code da sua conta como motor'],
139
+ ['/codex', 'usa o Codex da sua conta como motor'],
140
+ ['/chave', 'sua própria chave de API (Claude, Codex)'],
133
141
  ['/entrar', 'entra na sua conta Conecta Primo'],
134
142
  ['/conta', 'sua conta, plano e computadores conectados'],
135
143
  ['/conversas', 'suas conversas salvas, as mesmas do app e do celular'],
@@ -483,7 +491,7 @@ async function sendPrompt(userText) {
483
491
  // Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
484
492
  // não é motivo para criar pasta de projeto.
485
493
  ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
486
- motor: state.engine === 'claude-code' ? claudeEngine : api,
494
+ motor: motorDaVez(),
487
495
  });
488
496
 
489
497
  r = await rodarPasso();
@@ -656,6 +664,17 @@ async function runChat(promptForModel, userTextForMemory) {
656
664
 
657
665
  // ── Slash commands ─────────────────────────────────────────────────────
658
666
 
667
+ /* Qual motor executa este turno.
668
+ *
669
+ * Um `if` a mais aqui e um `if` a menos em qualquer outro lugar seria o
670
+ * caminho para o /codex funcionar no modo agente e não no chat, ou ao
671
+ * contrário. Um lugar só decide, e os dois modos chamam este. */
672
+ function motorDaVez() {
673
+ if (state.engine === 'claude-code') return claudeEngine;
674
+ if (state.engine === 'codex') return codexEngine;
675
+ return api;
676
+ }
677
+
659
678
  function isSlash(text) { return text.trim().startsWith('/'); }
660
679
 
661
680
  async function handleSlash(text) {
@@ -755,6 +774,72 @@ async function handleSlash(text) {
755
774
  break;
756
775
  }
757
776
 
777
+ /* A chave de API da própria pessoa. Guardada em ~/.primocode/
778
+ chaves.json com permissão 600 — nunca no config.json, que é o
779
+ arquivo que se cola num chamado de suporte. */
780
+ case 'chave': case 'chaves': {
781
+ const [qual, ...resto] = rest.split(/\s+/).filter(Boolean);
782
+ const valor = resto.join(' ');
783
+
784
+ if (!qual) {
785
+ console.log(c.white('\n Suas chaves de API'));
786
+ for (const p of chaves.listar()) {
787
+ console.log(' ' + c.white(p.nome.padEnd(8))
788
+ + (p.tem ? c.ok(p.disfarce) : c.dim('não configurada'))
789
+ + c.dim(` ${p.variavel}`));
790
+ }
791
+ console.log(c.dim('\n /chave claude sk-ant-… guarda a chave'));
792
+ console.log(c.dim(' /chave codex remover apaga a que estiver lá'));
793
+ console.log(c.muted('\n Sem chave própria, cada motor usa a conta em que você já entrou nele.'));
794
+ break;
795
+ }
796
+ if (!chaves.PROVEDORES[qual]) {
797
+ console.log(c.err(` Não conheço "${qual}". Use: ${Object.keys(chaves.PROVEDORES).join(', ')}`));
798
+ break;
799
+ }
800
+ if (valor === 'remover' || valor === 'apagar') {
801
+ const r = chaves.esquecer(qual);
802
+ console.log(r.ok ? c.ok(` Chave do ${chaves.PROVEDORES[qual].nome} apagada.`)
803
+ : c.muted(` ${r.error}`));
804
+ break;
805
+ }
806
+ if (!valor) {
807
+ const p = chaves.PROVEDORES[qual];
808
+ console.log(c.muted(` Uso: `) + c.brand(`/chave ${qual} ${p.prefixo}…`));
809
+ console.log(c.dim(` Pegue a sua em ${p.onde}`));
810
+ break;
811
+ }
812
+ const r = chaves.guardar(qual, valor);
813
+ if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
814
+ // O que se mostra de volta é o disfarce, nunca a chave: a linha
815
+ // fica no scrollback do terminal e no print que a pessoa manda.
816
+ console.log(c.ok(` Chave do ${r.nome} guardada.`) + c.muted(` ${r.disfarce}`));
817
+ console.log(c.dim(` Ela vai como ${chaves.PROVEDORES[qual].variavel} só quando o motor rodar.`));
818
+ break;
819
+ }
820
+
821
+ /* O Codex da conta da pessoa, com o modelo barato.
822
+ O protocolo é o MESMO do /claude — ver lib/codex-engine.js. */
823
+ case 'codex': {
824
+ if (rest.toLowerCase() === 'off') {
825
+ if (state.engine !== 'codex') { console.log(c.muted(' Já estava no motor padrão.')); break; }
826
+ state.engine = 'server';
827
+ console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
828
+ break;
829
+ }
830
+ console.log(c.dim(' Checando o Codex…'));
831
+ const r = codexEngine.garantirEngine();
832
+ if (!r.ok) {
833
+ console.log(c.err(` ${r.error}`));
834
+ if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
835
+ break;
836
+ }
837
+ state.engine = 'codex';
838
+ console.log(c.ok(` Motor: Codex (${r.modelo})`) + c.muted(' — na sua conta, não na minha'));
839
+ console.log(c.muted(' /codex off volta pro motor padrão.'));
840
+ break;
841
+ }
842
+
758
843
  case 'claude': {
759
844
  if (rest.toLowerCase() === 'off') {
760
845
  if (state.engine !== 'claude-code') { console.log(c.muted(' Já estava no motor padrão.')); break; }
@@ -771,7 +856,8 @@ async function handleSlash(text) {
771
856
  }
772
857
  state.engine = 'claude-code';
773
858
  const onde = r.caminho === 'A' ? 'sessão contínua via ORCA' : 'chamada avulsa (claude -p, sem ORCA)';
774
- console.log(c.ok(` Motor: Claude Code (${onde})`));
859
+ console.log(c.ok(` Motor: Claude Code (${onde})`)
860
+ + c.muted(` — ${claudeEngine.MODELO_CLAUDE || 'o padrão da sua conta'}, na sua conta`));
775
861
  console.log(c.muted(' /claude off volta pro motor padrão.'));
776
862
  break;
777
863
  }
package/lib/chaves.js ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * chaves.js — a chave de API da própria pessoa.
3
+ *
4
+ * "Você vai usar com a API que já está ali, e pode configurar sua própria
5
+ * API, por exemplo do Claude, do Codex, etc."
6
+ *
7
+ * ── ONDE ELAS MORAM ──────────────────────────────────────────────────────
8
+ * Em `~/.primocode/chaves.json`, com permissão 600 — o mesmo lugar e o mesmo
9
+ * cuidado da credencial da conta, e pela mesma razão: o `config.json` é o
10
+ * arquivo que alguém abre para conferir um ajuste, cola num chamado de
11
+ * suporte ou copia para outra máquina. Chave junto de preferência é chave que
12
+ * vaza por hábito, não por ataque.
13
+ *
14
+ * ── COMO ELAS CHEGAM AO MOTOR ────────────────────────────────────────────
15
+ * Como VARIÁVEL DE AMBIENTE do processo filho, que é o jeito que o Claude
16
+ * Code e o Codex já leem chave. Não se escreve em arquivo de configuração
17
+ * deles, não se mexe no login que a pessoa já fez: quem não configurar nada
18
+ * aqui continua usando a conta em que entrou nas ferramentas, e quem
19
+ * configurar passa a chave só naquela chamada.
20
+ *
21
+ * ── O QUE NUNCA APARECE ──────────────────────────────────────────────────
22
+ * A chave inteira. Nem ao listar, nem ao salvar, nem numa mensagem de erro. O
23
+ * que se mostra é o suficiente para a pessoa reconhecer qual é (o começo e o
24
+ * fim), porque quem tem três chaves precisa saber qual está ali — e é isso
25
+ * que evita ela colar a chave na tela para conferir.
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('fs');
31
+ const os = require('os');
32
+ const path = require('path');
33
+
34
+ const ARQUIVO = () => path.join(os.homedir(), '.primocode', 'chaves.json');
35
+
36
+ /**
37
+ * Os provedores que o PrimoCode sabe usar, e a variável que cada um lê.
38
+ *
39
+ * O nome da variável é o do provedor, não um inventado aqui: é o que as
40
+ * ferramentas já procuram, e inventar um significaria a chave certa no lugar
41
+ * errado — configurada, aceita, e ignorada na hora de valer.
42
+ */
43
+ const PROVEDORES = {
44
+ claude: {
45
+ nome: 'Claude',
46
+ variavel: 'ANTHROPIC_API_KEY',
47
+ prefixo: 'sk-ant-',
48
+ onde: 'https://console.anthropic.com/settings/keys',
49
+ motor: 'claude-code',
50
+ },
51
+ codex: {
52
+ nome: 'Codex',
53
+ variavel: 'OPENAI_API_KEY',
54
+ prefixo: 'sk-',
55
+ onde: 'https://platform.openai.com/api-keys',
56
+ motor: 'codex',
57
+ },
58
+ };
59
+
60
+ function ler() {
61
+ try { return JSON.parse(fs.readFileSync(ARQUIVO(), 'utf8')); }
62
+ catch { return {}; }
63
+ }
64
+
65
+ function gravar(dados) {
66
+ const caminho = ARQUIVO();
67
+ fs.mkdirSync(path.dirname(caminho), { recursive: true });
68
+ fs.writeFileSync(caminho, JSON.stringify(dados, null, 2), 'utf8');
69
+ // 600 = só o dono lê. No Windows o modo é ignorado pelo sistema de
70
+ // arquivos e a chamada não falha — lá quem protege é a pasta do perfil.
71
+ try { fs.chmodSync(caminho, 0o600); } catch {}
72
+ }
73
+
74
+ /** O bastante para reconhecer, nunca o bastante para usar. */
75
+ function disfarcar(chave) {
76
+ const s = String(chave || '');
77
+ if (s.length <= 12) return '•'.repeat(s.length);
78
+ return `${s.slice(0, 7)}…${s.slice(-4)}`;
79
+ }
80
+
81
+ /**
82
+ * Guarda uma chave. Recusa a que claramente não é do provedor pedido — o
83
+ * erro mais comum é colar a do Claude no Codex, e sem esta conferência isso
84
+ * só apareceria como "não autorizado" no meio de uma tarefa, longe da causa.
85
+ */
86
+ function guardar(provedor, chave) {
87
+ const p = PROVEDORES[provedor];
88
+ if (!p) return { ok: false, error: `não conheço "${provedor}". Use: ${Object.keys(PROVEDORES).join(', ')}` };
89
+ const limpa = String(chave || '').trim();
90
+ if (!limpa) return { ok: false, error: 'a chave veio vazia' };
91
+ if (limpa.length < 20) return { ok: false, error: 'isso é curto demais para ser uma chave' };
92
+ if (!limpa.startsWith(p.prefixo)) {
93
+ return { ok: false,
94
+ error: `uma chave do ${p.nome} começa com "${p.prefixo}", e essa não. Colou a do outro provedor?` };
95
+ }
96
+ // A do Claude também começa com "sk-", então o Codex precisa da recusa
97
+ // explícita: sem ela, `sk-ant-…` passaria como chave da OpenAI.
98
+ if (provedor === 'codex' && limpa.startsWith('sk-ant-')) {
99
+ return { ok: false, error: `essa é uma chave do Claude. Use /chave claude para ela.` };
100
+ }
101
+ const tudo = ler();
102
+ tudo[provedor] = limpa;
103
+ gravar(tudo);
104
+ return { ok: true, provedor, nome: p.nome, disfarce: disfarcar(limpa) };
105
+ }
106
+
107
+ function esquecer(provedor) {
108
+ const tudo = ler();
109
+ if (!(provedor in tudo)) return { ok: false, error: `não havia chave do ${provedor}` };
110
+ delete tudo[provedor];
111
+ gravar(tudo);
112
+ return { ok: true };
113
+ }
114
+
115
+ /** O que está configurado, disfarçado. */
116
+ function listar() {
117
+ const tudo = ler();
118
+ return Object.entries(PROVEDORES).map(([id, p]) => ({
119
+ id, nome: p.nome, variavel: p.variavel, onde: p.onde,
120
+ tem: typeof tudo[id] === 'string' && tudo[id].length > 0,
121
+ disfarce: tudo[id] ? disfarcar(tudo[id]) : null,
122
+ }));
123
+ }
124
+
125
+ /**
126
+ * O ambiente para rodar o motor de um provedor: o de agora, mais a chave.
127
+ *
128
+ * Sem chave configurada devolve o ambiente INTACTO — nem a variável vazia,
129
+ * que apagaria a chave que a pessoa já tem exportada no shell e faria a
130
+ * ferramenta parar de funcionar por ter sido "configurada".
131
+ */
132
+ function ambienteDe(provedor, base = process.env) {
133
+ const p = PROVEDORES[provedor];
134
+ if (!p) return base;
135
+ const chave = ler()[provedor];
136
+ if (!chave) return base;
137
+ return { ...base, [p.variavel]: chave };
138
+ }
139
+
140
+ /** Qual provedor um motor usa. */
141
+ function provedorDoMotor(motor) {
142
+ for (const [id, p] of Object.entries(PROVEDORES)) if (p.motor === motor) return id;
143
+ return null;
144
+ }
145
+
146
+ module.exports = {
147
+ PROVEDORES, guardar, esquecer, listar, ambienteDe, disfarcar,
148
+ provedorDoMotor, ARQUIVO,
149
+ };
@@ -17,6 +17,29 @@ const { execSync, execFileSync } = require('child_process');
17
17
  const MARCA_INICIO = '<<<PRIMOCODE>>>';
18
18
  const MARCA_FIM = '<<<FIM>>>';
19
19
 
20
+ /**
21
+ * O modelo que o /claude pede.
22
+ *
23
+ * "Ele usa o modelo Opus 5 [no Claude]."
24
+ *
25
+ * Vai o APELIDO (`opus`), não o identificador completo: o apelido é o que o
26
+ * Claude Code resolve para o Opus do dia, e um identificador fixo aqui
27
+ * envelheceria dentro de um pacote publicado no npm — a pessoa atualizaria o
28
+ * Claude Code, o modelo sairia de catálogo, e o PrimoCode pediria um modelo
29
+ * que não existe mais.
30
+ *
31
+ * Quem quiser outro põe PRIMOCODE_CLAUDE_MODELO. Vazio manda sem `--model`, e
32
+ * aí vale o padrão da conta da pessoa.
33
+ */
34
+ const MODELO_CLAUDE = process.env.PRIMOCODE_CLAUDE_MODELO !== undefined
35
+ ? process.env.PRIMOCODE_CLAUDE_MODELO
36
+ : 'opus';
37
+
38
+ /** Os argumentos do modelo, ou nenhum quando a escolha é do dono da conta. */
39
+ function argsDoModelo() {
40
+ return MODELO_CLAUDE ? ['--model', MODELO_CLAUDE] : [];
41
+ }
42
+
20
43
  /**
21
44
  * Acha o ÚLTIMO bloco marcado no texto (pode vir ruído de terminal — prompt
22
45
  * ANSI, texto solto — antes e depois) e devolve o JSON de dentro dele.
@@ -101,6 +124,7 @@ function lerJSON(bruto) {
101
124
  }
102
125
 
103
126
  const catalogo = require('./catalogo');
127
+ const chaves = require('./chaves');
104
128
 
105
129
  /** Serializa lib/catalogo.js em texto — única fonte de verdade, sem
106
130
  * duplicar a lista de ferramentas num arquivo separado. */
@@ -473,21 +497,44 @@ function linhaDoHistorico(m) {
473
497
  return `${m.role}: ${m.content}`;
474
498
  }
475
499
 
476
- async function requestActViaHeadless(payload, onEvent) {
500
+ /**
501
+ * O protocolo INTEIRO, mais a conversa, num prompt só.
502
+ *
503
+ * A versão anterior dizia "use a skill primocode-engine" e confiava que o
504
+ * headless a carregaria — quando não carregava, o Claude respondia sem bloco
505
+ * nenhum e o turno morria. Prompt maior, mas o motor tem contexto de sobra e
506
+ * o formato SEMPRE chega.
507
+ *
508
+ * SEM o cabeçalho YAML da skill: ele começa com "---", e um prompt que começa
509
+ * com traço o parser de argumentos lê como OPÇÃO — "error: unknown option
510
+ * '---'" na cara do usuário, com o protocolo inteiro despejado no terminal.
511
+ * Foi exatamente o que aconteceu.
512
+ *
513
+ * Isto é o que o /claude e o /codex têm em comum, e é de propósito que more
514
+ * num lugar só: são dois motores, e tem de ser UM protocolo.
515
+ */
516
+ function corpoDoProtocolo() {
517
+ return textoBootstrap().replace(/^---[\s\S]*?---\s*/, '');
518
+ }
519
+
520
+ /** O pedido de conserto: o protocolo de novo, mais o que saiu errado. Sem a
521
+ * conversa — o assunto aqui é a FORMA, e repetir o histórico inteiro só
522
+ * gastaria contexto no que não é o problema. */
523
+ function promptDeCorrecao(aviso) {
524
+ return corpoDoProtocolo() + '\n\n' + aviso;
525
+ }
526
+
527
+ function promptDoProtocolo(payload) {
477
528
  const historico = (payload.messages || []).map(linhaDoHistorico).join('\n\n');
478
- // O protocolo vai INTEIRO no prompt. A versão anterior dizia "use a skill
479
- // primocode-engine" e confiava que o headless a carregaria — quando não
480
- // carregava, o Claude respondia sem bloco nenhum e o turno morria. Prompt
481
- // maior, mas o Claude Code tem contexto de sobra, e o formato SEMPRE chega.
482
- // SEM o cabeçalho YAML da skill: ele começa com "---", e um prompt que
483
- // começa com traço o parser de argumentos do claude lê como OPÇÃO —
484
- // "error: unknown option '---'" na cara do usuário, com o protocolo
485
- // inteiro despejado no terminal. Foi exatamente o que aconteceu.
486
- const corpoBootstrap = textoBootstrap().replace(/^---[\s\S]*?---\s*/, '');
487
- const prompt = corpoBootstrap
529
+ return corpoDoProtocolo()
488
530
  + '\n\nIMPORTANTE: no JSON do bloco, escreva quebras de linha como \\n (escapadas), nunca linha de verdade.'
489
531
  + '\n\n# A conversa até aqui\n\n' + historico
490
532
  + '\n\nResponda com exatamente UM bloco marcado.';
533
+ }
534
+
535
+ async function requestActViaHeadless(payload, onEvent) {
536
+ const corpoBootstrap = corpoDoProtocolo();
537
+ const prompt = promptDoProtocolo(payload);
491
538
  // --dangerously-skip-permissions: mesma razão do caminho ORCA acima —
492
539
  // a skill já proíbe o Claude Code de usar suas próprias ferramentas
493
540
  // nativas; a flag só evita que ele pare pedindo permissão pra si mesmo
@@ -499,7 +546,9 @@ async function requestActViaHeadless(payload, onEvent) {
499
546
  '-p',
500
547
  '--output-format', 'json',
501
548
  '--dangerously-skip-permissions',
502
- ], { encoding: 'utf-8', timeout: 180000, input: prompt, maxBuffer: 32 * 1024 * 1024 });
549
+ ...argsDoModelo(),
550
+ ], { encoding: 'utf-8', timeout: 180000, input: prompt, maxBuffer: 32 * 1024 * 1024,
551
+ env: chaves.ambienteDe('claude') });
503
552
  const r = JSON.parse(saida);
504
553
  // A correção é uma segunda ida ao motor, com o mesmo protocolo e o
505
554
  // diagnóstico do que saiu errado. Barata: o prompt é curto, e evita que
@@ -507,9 +556,11 @@ async function requestActViaHeadless(payload, onEvent) {
507
556
  const pedirCorrecao = (aviso) => {
508
557
  const s = execFileSync('claude', [
509
558
  '-p', '--output-format', 'json', '--dangerously-skip-permissions',
559
+ ...argsDoModelo(),
510
560
  ], {
511
561
  encoding: 'utf-8', timeout: 120000, maxBuffer: 32 * 1024 * 1024,
512
- input: corpoBootstrap + '\n\n' + aviso,
562
+ env: chaves.ambienteDe('claude'),
563
+ input: promptDeCorrecao(aviso),
513
564
  });
514
565
  return JSON.parse(s).result || '';
515
566
  };
@@ -543,6 +594,13 @@ async function requestAct(server, payload, onEvent) {
543
594
  module.exports = {
544
595
  MARCA_INICIO, MARCA_FIM, extrairDecisao,
545
596
  catalogoParaTexto, textoBootstrap,
597
+ // O protocolo sai daqui para o lib/codex-engine.js usar o MESMO. Ele é o
598
+ // que faz um motor de fora virar cérebro do PrimoCode: o texto que
599
+ // ensina o formato, a leitura do bloco marcado e a volta para consertar
600
+ // o que veio torto. Reimplementar aquilo no outro motor seria manter dois
601
+ // protocolos que divergem na primeira correção de um deles.
602
+ linhaDoHistorico, nomesDasTools, promptDoProtocolo, promptDeCorrecao,
603
+ MODELO_CLAUDE, argsDoModelo,
546
604
  SKILL_DIR, SKILL_PATH, garantirSkill,
547
605
  temClaudeCode, temOrca, orcaAlcancavel, escolherCaminho,
548
606
  garantirEngine, requestAct, emitirDecisao,
@@ -0,0 +1,201 @@
1
+ /**
2
+ * codex-engine.js — motor do agente rodando no Codex da SUA conta.
3
+ *
4
+ * "Você pode usar a barra codex para usar os modelos com a sua conta. No
5
+ * Claude ele usa o modelo Opus 5; no Codex ele vai usar o modelo baratinho
6
+ * ali."
7
+ *
8
+ * ── É UM MOTOR NOVO, NÃO UM PROTOCOLO NOVO ───────────────────────────────
9
+ * O que ensina um modelo de fora a ser cérebro do PrimoCode — o texto do
10
+ * protocolo, a leitura do bloco marcado, a volta para consertar o que veio
11
+ * torto — mora inteiro no claude-engine.js e é IMPORTADO daqui. Copiar
12
+ * aquilo para cá daria dois protocolos, e o segundo começaria a divergir do
13
+ * primeiro na primeira correção feita só de um lado — sem erro nenhum
14
+ * aparecendo, só um dos motores ficando pior que o outro com o tempo.
15
+ *
16
+ * O que muda de verdade entre os dois é uma coisa só: qual programa se chama
17
+ * e com quais argumentos. É isso, e só isso, que este arquivo tem.
18
+ *
19
+ * ── POR QUE LER A SAÍDA COMO TEXTO CRU ───────────────────────────────────
20
+ * O `claude -p` tem `--output-format json` e o formato é estável. O `codex`
21
+ * mudou de forma mais de uma vez, e amarrar-se ao JSON dele seria escolher
22
+ * quebrar no dia em que ele mudar de novo — calado, porque a saída
23
+ * continuaria vindo, só que com outras chaves.
24
+ *
25
+ * Então aqui se lê o texto e se procura o bloco marcado nele, que é o que o
26
+ * `extrairDecisao` já faz: ele varre ruído de terminal e fica com o último
27
+ * bloco legível. Enquanto o Codex escrever o bloco em algum lugar da saída,
28
+ * este motor funciona — independente de como ele embrulhe o resto.
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ const { execSync, execFileSync } = require('child_process');
34
+
35
+ const claude = require('./claude-engine');
36
+ const chaves = require('./chaves');
37
+
38
+ /**
39
+ * O modelo barato do Codex.
40
+ *
41
+ * O /claude vai no Opus porque ali o que se quer é a decisão mais certa. Aqui
42
+ * o pedido foi o contrário — "o modelo baratinho" — e faz sentido: quem liga
43
+ * o /codex está usando a franquia da própria conta, e o motor do PrimoCode dá
44
+ * muitas voltas por tarefa.
45
+ *
46
+ * Vazio manda sem `--model`, e vale o padrão da conta. E se o Codex recusar
47
+ * este modelo, o motor tenta de novo SEM ele em vez de morrer — ver
48
+ * `chamarCodex`.
49
+ */
50
+ const MODELO_CODEX = process.env.PRIMOCODE_CODEX_MODELO !== undefined
51
+ ? process.env.PRIMOCODE_CODEX_MODELO
52
+ : 'gpt-5-mini';
53
+
54
+ /** O programa. Trocável para o teste pôr um de mentira no lugar. */
55
+ const CODEX = process.env.PRIMOCODE_CODEX_BIN || 'codex';
56
+
57
+ const TEMPO = 180000;
58
+ const BUFFER = 32 * 1024 * 1024;
59
+
60
+ function temCodex(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
61
+ try { executor(`${CODEX} --version`); return true; }
62
+ catch { return false; }
63
+ }
64
+
65
+ /**
66
+ * O que o `codex exec` desta máquina aceita.
67
+ *
68
+ * Passar uma opção que a versão instalada não conhece não dá "opção
69
+ * desconhecida" e pronto: dá uma saída de ajuda no lugar da resposta, e o
70
+ * motor devolveria "não consegui ler a decisão" em toda rodada — com a
71
+ * pessoa sem nenhuma pista de que o problema é uma flag.
72
+ *
73
+ * Perguntar ao `--help` custa uma chamada por sessão e responde com o que
74
+ * ESTA versão faz, em vez do que a versão de hoje fazia quando isto foi
75
+ * escrito.
76
+ */
77
+ let _ajuda = null;
78
+ function ajudaDoExec(executor) {
79
+ if (_ajuda !== null) return _ajuda;
80
+ const rodar = executor || ((args) => execFileSync(CODEX, args,
81
+ { encoding: 'utf-8', timeout: 20000, stdio: ['ignore', 'pipe', 'ignore'] }));
82
+ try { _ajuda = String(rodar(['exec', '--help']) || ''); }
83
+ catch { _ajuda = ''; } // sem ajuda: segue com o mínimo
84
+ return _ajuda;
85
+ }
86
+
87
+ function esquecerAjuda() { _ajuda = null; }
88
+
89
+ /**
90
+ * Os argumentos da chamada.
91
+ *
92
+ * `--skip-git-repo-check` só entra se esta versão o conhecer: sem ele, o
93
+ * Codex se recusa a rodar fora de um repositório git — e o PrimoCode trabalha
94
+ * em pasta comum o tempo todo.
95
+ */
96
+ function argsDoExec({ comModelo = true, ajuda = null } = {}) {
97
+ const h = ajuda === null ? ajudaDoExec() : ajuda;
98
+ const args = ['exec'];
99
+ if (h.includes('--skip-git-repo-check')) args.push('--skip-git-repo-check');
100
+ // O Codex pede confirmação para agir na máquina. Aqui ele não age: o
101
+ // protocolo proíbe ferramenta nativa, quem executa é o PrimoCode. A opção
102
+ // só evita que ele pare esperando uma resposta que ninguém vai dar.
103
+ if (h.includes('--dangerously-bypass-approvals-and-sandbox')) {
104
+ args.push('--dangerously-bypass-approvals-and-sandbox');
105
+ } else if (h.includes('--full-auto')) {
106
+ args.push('--full-auto');
107
+ }
108
+ if (comModelo && MODELO_CODEX) args.push('--model', MODELO_CODEX);
109
+ // O prompt vai por STDIN: por argumento ele teria limite de tamanho (a
110
+ // conversa cresce) e qualquer texto começando com traço seria lido como
111
+ // opção. `-` é como o exec pede para ler da entrada.
112
+ args.push('-');
113
+ return args;
114
+ }
115
+
116
+ /** Um modelo que a conta não tem é o erro mais provável desta chamada — e o
117
+ * único que dá para consertar sozinho. */
118
+ function recusouOModelo(texto) {
119
+ return /model|modelo/i.test(texto)
120
+ && /not (found|available|supported)|unknown|invalid|does not|inexistente|inválid/i.test(texto);
121
+ }
122
+
123
+ /**
124
+ * Chama o Codex e devolve o texto. Se ele recusar o modelo, tenta UMA vez sem
125
+ * `--model` e avisa — cair para o padrão da conta é melhor que a tarefa
126
+ * morrer por causa de um nome que mudou de catálogo.
127
+ */
128
+ function chamarCodex(prompt, { executor, avisar = () => {} } = {}) {
129
+ // A chave própria da pessoa, quando ela configurou uma. Sem configuração
130
+ // o ambiente vai INTACTO — passar a variável vazia apagaria a chave que
131
+ // ela já tem exportada no shell, e o Codex pararia de funcionar por ter
132
+ // sido "configurado".
133
+ const rodar = executor || ((args, entrada) => execFileSync(CODEX, args, {
134
+ encoding: 'utf-8', timeout: TEMPO, input: entrada, maxBuffer: BUFFER,
135
+ env: chaves.ambienteDe('codex'),
136
+ }));
137
+ try {
138
+ return rodar(argsDoExec(), prompt);
139
+ } catch (e) {
140
+ const detalhe = `${e.message || ''}\n${(e.stderr || '')}\n${(e.stdout || '')}`;
141
+ if (MODELO_CODEX && recusouOModelo(detalhe)) {
142
+ avisar(`o Codex não aceitou o modelo ${MODELO_CODEX} — indo com o padrão da sua conta`);
143
+ return rodar(argsDoExec({ comModelo: false }), prompt);
144
+ }
145
+ throw new Error(primeiraLinhaUtil(detalhe) || e.message);
146
+ }
147
+ }
148
+
149
+ /** A primeira linha que diz alguma coisa. O erro do Codex costuma vir com
150
+ * banner, versão e caminho antes do motivo, e jogar tudo isso na tela é
151
+ * esconder o motivo no meio. */
152
+ function primeiraLinhaUtil(texto) {
153
+ for (const linha of String(texto || '').split('\n')) {
154
+ const l = linha.trim();
155
+ if (l && !/^(Command failed|codex\b|\s*$)/i.test(l)) return l.slice(0, 300);
156
+ }
157
+ return '';
158
+ }
159
+
160
+ function garantirEngine() {
161
+ if (!temCodex()) {
162
+ return {
163
+ ok: false,
164
+ error: 'O Codex não está instalado nesta máquina.',
165
+ comoResolver: 'Instale o Codex CLI da OpenAI, entre na sua conta com `codex login`, '
166
+ + 'e rode /codex de novo.',
167
+ };
168
+ }
169
+ return { ok: true, modelo: MODELO_CODEX || '(o padrão da sua conta)' };
170
+ }
171
+
172
+ /** Mesma forma de api.requestAct(server, payload, onEvent) — é isso que deixa
173
+ * o lib/act.js trocar de motor sem saber de nada disto. */
174
+ async function requestAct(server, payload, onEvent) {
175
+ try {
176
+ const pronto = garantirEngine();
177
+ if (!pronto.ok) {
178
+ onEvent({ erro: `${pronto.error} ${pronto.comoResolver || ''}`.trim() });
179
+ return;
180
+ }
181
+ const prompt = claude.promptDoProtocolo(payload);
182
+ const saida = chamarCodex(prompt, { avisar: (m) => onEvent({ aviso: m }) });
183
+
184
+ // A correção é uma segunda ida ao motor, com o mesmo protocolo e o
185
+ // diagnóstico do que saiu errado. Barata, e evita que um erro de forma
186
+ // do motor vire erro na cara de quem pediu a tarefa.
187
+ const pedirCorrecao = (aviso) => chamarCodex(claude.promptDeCorrecao(aviso));
188
+
189
+ await claude.emitirDecisao(saida, onEvent, pedirCorrecao, 0,
190
+ claude.nomesDasTools(payload));
191
+ } catch (e) {
192
+ onEvent({ erro: e.message });
193
+ }
194
+ }
195
+
196
+ module.exports = {
197
+ MODELO_CODEX, CODEX,
198
+ temCodex, garantirEngine, requestAct,
199
+ argsDoExec, ajudaDoExec, esquecerAjuda,
200
+ chamarCodex, recusouOModelo, primeiraLinhaUtil,
201
+ };
package/lib/config.js CHANGED
@@ -19,8 +19,21 @@ const ROOT_DIR = path.join(HOME, 'primocode');
19
19
  const CONFIG_DIR = path.join(HOME, '.primocode');
20
20
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
21
21
  const HISTORY_FILE = path.join(CONFIG_DIR, 'history.jsonl');
22
+ const CONTA_FILE = path.join(CONFIG_DIR, 'conta.json');
22
23
  const GENERAL_MEMORY = path.join(ROOT_DIR, 'memory-geral.md');
23
24
 
25
+ /* O token da conta, lido DIRETO do arquivo.
26
+ *
27
+ * Sem `require('./conta.js')` de propósito: o conta.js já carrega o
28
+ * config.js, e um exigir o outro nos dois sentidos deixa um dos dois com
29
+ * metade do módulo na mão — em Node isso não dá erro, dá `undefined` numa
30
+ * função no meio de um turno. Ler quatro linhas de JSON é mais barato que
31
+ * essa aula. */
32
+ function tokenDaConta() {
33
+ try { return JSON.parse(fs.readFileSync(CONTA_FILE, 'utf8')).token || null; }
34
+ catch { return null; }
35
+ }
36
+
24
37
  const GENERAL_MEMORY_TEMPLATE = `# Memória Geral
25
38
 
26
39
  Informações que valem para todos os projetos. O PrimoCode lê este arquivo
@@ -254,7 +267,19 @@ function loadConfig() {
254
267
  // Esforço, estilo Claude Code (/effort). É o que o usuário escolhe; o
255
268
  // modelo é derivado dele. Guardado para durar entre sessões.
256
269
  effort: process.env.PRIMOCODE_EFFORT || file.effort || 'medium',
257
- token: process.env.PRIMOCODE_TOKEN || file.token || null,
270
+ /* O token que vai ao servidor.
271
+ *
272
+ * O da CONTA entra por último, como padrão de quem não configurou
273
+ * nada — é ele que faz o servidor reconhecer Premium/Super e liberar
274
+ * o teto. Entra AQUI e não em cada chamada porque são cinco lugares
275
+ * que mandam pedido: o sexto que alguém escrever esqueceria, e a
276
+ * pessoa veria o limite bater com a assinatura em dia.
277
+ *
278
+ * A ordem é a de quem manda mais: variável de ambiente ganha do
279
+ * arquivo, e o arquivo ganha da conta. Quem hospeda o próprio
280
+ * servidor e configurou um token não pode ter isso trocado por
281
+ * entrar numa conta. */
282
+ token: process.env.PRIMOCODE_TOKEN || file.token || tokenDaConta(),
258
283
  autoApply: file.autoApply === true,
259
284
  currentProject: file.currentProject || null,
260
285
  userName: file.userName || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.31.0",
3
+ "version": "8.32.0",
4
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.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"