primocode 8.31.0 → 8.33.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.
@@ -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/lib/modo.js CHANGED
@@ -109,10 +109,50 @@ function escolherModo(texto, ctx = {}) {
109
109
  if (RE_SAUDACAO.test(t)) return { modo: 'chat', motivo: 'cumprimento' };
110
110
  if (RE_SATISFEITO.test(t)) return { modo: 'chat', motivo: 'está satisfeito' };
111
111
 
112
- // Pergunta sobre ele mesmo, e pergunta conceitual, ganham do verbo:
113
- // "como faço para criar um site?" é dúvida, não pedido de execução.
112
+ // Pergunta sobre ele mesmo ganha do verbo: "o que você faz?" é dúvida.
114
113
  if (RE_SOBRE_ELE.test(t)) return { modo: 'chat', motivo: 'pergunta sobre ele' };
115
- if (RE_CONCEITUAL.test(t)) return { modo: 'chat', motivo: 'pergunta conceitual' };
114
+
115
+ /* ── A ENCOMENDA GANHA DA PERGUNTA ───────────────────────────────────
116
+ "como faço para criar um site?" é dúvida. Mas "pesquise COMO FUNCIONA
117
+ o WAV e escreva o resultado em wav.md" é encomenda: tem verbo de ação E
118
+ diz o arquivo que deve nascer. A frase conceitual ali é o ASSUNTO do
119
+ trabalho, não o pedido.
120
+
121
+ Antes, a checagem conceitual vinha antes destas duas e ganhava sempre.
122
+ O efeito era mudo: o pedido virava conversa, a resposta vinha certa, e
123
+ o arquivo NUNCA nascia — com um "não posso criar arquivos" no fim, que
124
+ é a única pista, e ela chega depois de a pessoa já ter esperado.
125
+ Aconteceu de verdade num teste: `wav.md` não existiu, e "crie uma
126
+ apresentação sobre X: o que é, o que faz…" foi para o chat porque o
127
+ ROTEIRO DOS SLIDES tinha "o que é" dentro.
128
+
129
+ O que separa uma coisa da outra é ONDE a frase conceitual está. No
130
+ COMEÇO ela governa a frase inteira e é a pergunta: "como faço para
131
+ criar um site?", "por que o node é assíncrono". Mais para dentro, ela
132
+ é o assunto de um pedido que já começou com outra coisa: "pesquise
133
+ como funciona…", "crie uma apresentação: o que é, o que faz…".
134
+
135
+ E nem o arquivo nomeado escapa da regra: "pra que serve o package.json"
136
+ cita um arquivo e continua sendo pergunta. O que decide é a POSIÇÃO,
137
+ não a presença. */
138
+ const conceitual = t.match(RE_CONCEITUAL);
139
+ if (conceitual) {
140
+ /* O que decide é o que vem ANTES dela. Se a frase já tinha começado
141
+ com um verbo de ação, a pergunta é o assunto do trabalho:
142
+
143
+ "pesquise | como funciona o WAV e escreva em wav.md" → encomenda
144
+ "crie uma apresentação: | o que é, o que faz…" → encomenda
145
+ "| pra que serve o package.json" → pergunta
146
+ "| como funciona o WAV?" → pergunta
147
+ "me | explica o que é recursão" → pergunta
148
+
149
+ Contar caracteres não serve: "pesquise" tem nove letras e "crie uma
150
+ apresentação de 4 slides sobre o PrimoCode:" tem quarenta e três, e
151
+ os dois são a mesma coisa. Quem manda é o verbo, não a régua. */
152
+ const antes = t.slice(0, conceitual.index);
153
+ const jaEraOrdem = RE_VERBO.test(antes) || RE_PECA.test(antes);
154
+ if (!jaEraOrdem) return { modo: 'chat', motivo: 'pergunta conceitual' };
155
+ }
116
156
 
117
157
  // "refaz", "de novo", "continua" — retomada, com ou sem contexto.
118
158
  if (RE_CONTINUACAO.test(t)) return { modo: 'primocode', motivo: 'retoma a tarefa' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.31.0",
3
+ "version": "8.33.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 && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"