primocode 9.8.0-beta.1 → 9.8.0-beta.2

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 v9.8.0-beta.1
1
+ # PrimoCode v9.8.0-beta.2
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 v9.8.0-beta.1
12
+ ▐▛███▜▌ Primo Code v9.8.0-beta.2
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -288,7 +288,7 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
288
288
  | `/skills` · `/marketplace` | O que ele sabe fazer bem. Tela navegável pelas setas, e "New Skill" para escrever a sua |
289
289
  | `/connectors` | Quem está atendendo os seus pedidos (só mostra — não há o que configurar) |
290
290
  | `/effort` | O esforço do agente, num slider (←/→, Enter confirma): do `low` ao `ultracode`. O modelo sai daí |
291
- | `/api` | Roda com a SUA chave (Gemini, ChatGPT, Claude, Groq, OpenRouter, DeepSeek), direto do seu computador, sem o limite do servidor. `/api modelo <nome>` troca o modelo · `/api off` volta ao servidor |
291
+ | `/api` | Roda com a SUA assinatura (Claude Code, Codex, sem chave) ou a SUA chave (Gemini, ChatGPT, Claude, Groq, OpenRouter, DeepSeek), direto do seu computador, sem o limite do servidor. `/api modelo <nome>` troca o modelo · `/api off` volta ao servidor |
292
292
  | `/attach <arquivo>` | Anexa arquivo ao próximo pedido |
293
293
  | `/detach` · `/files` | Remove / lista anexos |
294
294
  | `/all` | Lista as 46 ferramentas que ele sabe usar, por grupo |
package/bin/primocode.js CHANGED
@@ -198,11 +198,12 @@ async function comandoApi(rest) {
198
198
  return;
199
199
  }
200
200
 
201
- let id = ids.find((k) => k === arg.toLowerCase() || P[k].nome.toLowerCase().startsWith(arg.toLowerCase()) && arg);
201
+ const apelido = { claude: 'claude-code', gpt: 'chatgpt', openai: 'chatgpt' }[arg.toLowerCase()];
202
+ let id = apelido || ids.find((k) => k === arg.toLowerCase() || P[k].nome.toLowerCase().startsWith(arg.toLowerCase()) && arg);
202
203
  if (!id) {
203
204
  console.log('');
204
- console.log(' ' + c.bold('Com qual IA você quer rodar?') + c.muted(' a sua chave, direto daqui, sem o limite do servidor'));
205
- if (atual) console.log(' ' + c.muted('agora: ') + c.white(`${atual.nome} · ${atual.modelo} · chave ${provedorLocal.mascarar(atual.chave)}`));
205
+ console.log(' ' + c.bold('Com qual IA você quer rodar?') + c.muted(' a sua chave ou a sua assinatura, sem o limite do servidor'));
206
+ if (atual) console.log(' ' + c.muted('agora: ') + c.white(atual.cli ? atual.nome : `${atual.nome} · ${atual.modelo} · chave ${provedorLocal.mascarar(atual.chave)}`));
206
207
  ids.forEach((k, i) => console.log(` ${c.brand(String(i + 1))} ${c.white(P[k].nome.padEnd(24))}${c.dim(P[k].dica)}`));
207
208
  console.log(` ${c.brand('0')} ${c.white('Servidor do PrimoCode'.padEnd(24))}${c.dim('sem chave, com o limite do plano')}`);
208
209
  const escolha = String(await ask(` ${c.brand('?')} Número: `)).trim();
@@ -212,6 +213,18 @@ async function comandoApi(rest) {
212
213
  }
213
214
 
214
215
  const p = P[id];
216
+ // Claude Code e Codex não pedem chave: basta a ferramenta estar instalada
217
+ // e logada. Quem pensa é ela; quem executa é o PrimoCode.
218
+ if (p.cli) {
219
+ if (!require('../lib/local/motor-cli').instalado(id)) {
220
+ console.log(c.warn(` O ${p.nome} não está instalado nesta máquina.`) + c.muted(` Veja ${p.onde}`));
221
+ return;
222
+ }
223
+ const salvo = provedorLocal.salvar({ provedor: id, modelo: p.modelos[0] });
224
+ console.log(c.ok(` Pronto: ${salvo.nome}`) + c.muted(' · usa a sua assinatura, sem chave'));
225
+ console.log(c.dim(` O ${p.nome} decide, o PrimoCode executa. /api off volta ao servidor`));
226
+ return;
227
+ }
215
228
  console.log(c.muted(` Pegue a chave em `) + c.white(p.onde));
216
229
  const chave = String(await askSecret(` ${c.brand('?')} Cole a chave do ${p.nome} (não aparece na tela): `)).trim();
217
230
  if (!chave) { console.log(c.muted(' Nada mudou.')); return; }
package/lib/api.js CHANGED
@@ -206,8 +206,11 @@ function requestStream(serverUrl, endpoint, payload, onEvent, token, sinal) {
206
206
  O resto (login, transcrição, conectores) continua no servidor. */
207
207
  function local() {
208
208
  try {
209
- const motor = require('./local/motor.js');
210
- return motor.ativo() ? motor : null;
209
+ const atual = require('./local/provedor.js').atual();
210
+ if (!atual) return null;
211
+ // Claude Code e Codex: a ferramenta de fora é o cérebro.
212
+ if (atual.cli) return require('./local/motor-cli.js').criar(atual.id);
213
+ return require('./local/motor.js');
211
214
  } catch { return null; }
212
215
  }
213
216
 
@@ -0,0 +1,630 @@
1
+ /**
2
+ * claude-engine.js — motor alternativo do agente: Claude Code local em vez
3
+ * de OpenRouter/Groq via primocode-server.
4
+ *
5
+ * Implementa a MESMA forma que lib/api.js (requestAct(server, payload,
6
+ * onEvent)) — é o que permite lib/act.js trocar de motor sem saber disso.
7
+ * Ver docs/superpowers/specs/2026-08-15-claude-engine-design.md.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const { execSync, execFileSync } = require('child_process');
16
+
17
+ /* ── O MOTOR EXTERNO NÃO APARECE ────────────────────────────────────────
18
+ *
19
+ * "O Primo Code abre esses processos ocultamente (em segundo plano) e usa
20
+ * apenas o modelo dessas ferramentas. Toda a interface, comandos, agentes e
21
+ * experiência continuam sendo 100% do Primo Code."
22
+ *
23
+ * `execFileSync` sem `stdio` HERDA o stderr do pai — é o padrão do Node, e não
24
+ * parece nada até o dia em que o motor imprime um aviso de versão, de
25
+ * atualização ou de configuração. Aquilo cai no meio da tela do PrimoCode, com
26
+ * a cara e o vocabulário de outro produto, no meio de uma tarefa.
27
+ *
28
+ * Com o stderr CAPTURADO ele some da tela e ganha um segundo uso: `e.stderr` só
29
+ * existe quando é canalizado, e o tratamento de erro daqui (e o do
30
+ * codex-engine) já lia esse campo — estava lendo `null` desde sempre, e a
31
+ * mensagem de falha do motor saía sem o motivo que ele mesmo tinha escrito.
32
+ *
33
+ * `windowsHide` é a outra metade do "ocultamente": sem ele, cada chamada
34
+ * pisca um console preto na frente de quem usa Windows. */
35
+ const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
36
+
37
+
38
+ const MARCA_INICIO = '<<<PRIMOCODE>>>';
39
+ const MARCA_FIM = '<<<FIM>>>';
40
+
41
+ /**
42
+ * O modelo que o /claude pede.
43
+ *
44
+ * "Ele usa o modelo Opus 5 [no Claude]."
45
+ *
46
+ * Vai o APELIDO (`opus`), não o identificador completo: o apelido é o que o
47
+ * Claude Code resolve para o Opus do dia, e um identificador fixo aqui
48
+ * envelheceria dentro de um pacote publicado no npm — a pessoa atualizaria o
49
+ * Claude Code, o modelo sairia de catálogo, e o PrimoCode pediria um modelo
50
+ * que não existe mais.
51
+ *
52
+ * Quem quiser outro põe PRIMOCODE_CLAUDE_MODELO. Vazio manda sem `--model`, e
53
+ * aí vale o padrão da conta da pessoa.
54
+ */
55
+ const MODELO_CLAUDE = process.env.PRIMOCODE_CLAUDE_MODELO !== undefined
56
+ ? process.env.PRIMOCODE_CLAUDE_MODELO
57
+ : 'opus';
58
+
59
+ /** Os argumentos do modelo, ou nenhum quando a escolha é do dono da conta. */
60
+ function argsDoModelo() {
61
+ return MODELO_CLAUDE ? ['--model', MODELO_CLAUDE] : [];
62
+ }
63
+
64
+ /**
65
+ * Acha o ÚLTIMO bloco marcado no texto (pode vir ruído de terminal — prompt
66
+ * ANSI, texto solto — antes e depois) e devolve o JSON de dentro dele.
67
+ * null quando não achou bloco válido: quem chama decide o que fazer.
68
+ */
69
+ function extrairDecisao(texto) {
70
+ const t = String(texto || '');
71
+
72
+ // Varre TODOS os blocos completos e fica com o último que dá para ler.
73
+ //
74
+ // A versão anterior fazia lastIndexOf(INICIO) e depois procurava o FIM
75
+ // para frente. Bastava o modelo escrever a marca de abertura mais uma vez
76
+ // — explicando o formato, ou começando um bloco e desistindo — para o FIM
77
+ // não existir depois dela: a extração devolvia null e o protocolo CRU ia
78
+ // parar na tela do usuário, com tool_call e tudo. Foi o que aconteceu num
79
+ // pedido real de vídeo.
80
+ const blocos = [];
81
+ let i = 0;
82
+ while (true) {
83
+ const inicio = t.indexOf(MARCA_INICIO, i);
84
+ if (inicio === -1) break;
85
+ const fim = t.indexOf(MARCA_FIM, inicio + MARCA_INICIO.length);
86
+ if (fim === -1) break; // bloco aberto e não fechado: ignora
87
+ blocos.push(t.slice(inicio + MARCA_INICIO.length, fim));
88
+ i = fim + MARCA_FIM.length;
89
+ }
90
+
91
+ for (let k = blocos.length - 1; k >= 0; k--) {
92
+ const lido = lerJSON(blocos[k]);
93
+ if (lido) return lido;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /** JSON com quebra de linha CRUA dentro das aspas — o jeito mais comum de o
99
+ * motor falhar. O Claude escreve o content de um write_file com linhas de
100
+ * verdade em vez de \n, o JSON.parse recusa, e o turno inteiro morria com
101
+ * "decisão que não consegui ler". O conserto é mecânico: dentro de string,
102
+ * controle vira escape. */
103
+ function consertarControles(s) {
104
+ let saida = '';
105
+ let dentro = false, escapa = false;
106
+ for (const c of String(s || '')) {
107
+ if (dentro && !escapa && (c === '\n' || c === '\r' || c === '\t')) {
108
+ saida += c === '\n' ? '\\n' : c === '\r' ? '\\r' : '\\t';
109
+ continue;
110
+ }
111
+ if (escapa) { escapa = false; saida += c; continue; }
112
+ if (c === '\\') { escapa = true; saida += c; continue; }
113
+ if (c === '"') dentro = !dentro;
114
+ saida += c;
115
+ }
116
+ return saida;
117
+ }
118
+
119
+ /** O JSON do bloco, tolerando o que o modelo costuma acrescentar sem querer. */
120
+ function lerJSON(bruto) {
121
+ let s = String(bruto || '').trim();
122
+ // Cerca de markdown em volta do JSON — o modelo põe por hábito.
123
+ s = s.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
124
+ try { return JSON.parse(s); } catch {}
125
+ try { return JSON.parse(consertarControles(s)); } catch {}
126
+ s = consertarControles(s);
127
+ // Sobrou texto depois do objeto? Corta no fecha-chaves que casa com o primeiro.
128
+ const abre = s.indexOf('{');
129
+ if (abre === -1) return null;
130
+ let nivel = 0, dentro = false, escapa = false;
131
+ for (let k = abre; k < s.length; k++) {
132
+ const c = s[k];
133
+ if (escapa) { escapa = false; continue; }
134
+ if (c === '\\') { escapa = true; continue; }
135
+ if (c === '"') { dentro = !dentro; continue; }
136
+ if (dentro) continue;
137
+ if (c === '{') nivel++;
138
+ else if (c === '}' && --nivel === 0) {
139
+ const trecho = s.slice(abre, k + 1);
140
+ try { return JSON.parse(trecho); } catch {}
141
+ try { return JSON.parse(consertarControles(trecho)); } catch { return null; }
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+
147
+ const catalogo = require('./catalogo');
148
+
149
+ /** Serializa lib/catalogo.js em texto — única fonte de verdade, sem
150
+ * duplicar a lista de ferramentas num arquivo separado. */
151
+ function catalogoParaTexto() {
152
+ const linhas = [];
153
+ for (const grupo of catalogo.GRUPOS) {
154
+ linhas.push(`## ${grupo.nome} — ${grupo.resumo}`);
155
+ for (const item of grupo.itens) {
156
+ const args = item.args ? ` | args: ${item.args}` : ' | sem args';
157
+ linhas.push(`- ${item.tool}: ${item.faz}${args}`);
158
+ }
159
+ linhas.push('');
160
+ }
161
+ return linhas.join('\n').trim();
162
+ }
163
+
164
+ /** Texto completo que ensina o Claude Code a se comportar como motor do
165
+ * PrimoCode. Vira o conteúdo de ~/.claude/skills/primocode-engine/SKILL.md
166
+ * (Task 4) e é referenciado na primeira mensagem de cada sessão. */
167
+ function textoBootstrap() {
168
+ return `---
169
+ name: primocode-engine
170
+ description: Skill instalada pelo PrimoCode para o modo /claude — motor de decisão do agente, não um agente autônomo.
171
+ ---
172
+
173
+ # PrimoCode — modo motor
174
+
175
+ Você está sendo usado como o "cérebro" do PrimoCode, um CLI que executa
176
+ tarefas de engenharia. Não use suas próprias ferramentas nativas (Read,
177
+ Write, Edit, Bash, Glob, Grep, WebFetch, etc.) — quem executa é o
178
+ PrimoCode, não você. Sua única saída é dizer QUAL ferramenta do PrimoCode
179
+ chamar, com quais argumentos, ou a resposta final.
180
+
181
+ ## Ferramentas disponíveis
182
+
183
+ ${catalogoParaTexto()}
184
+
185
+ ## Formato de resposta — obrigatório
186
+
187
+ Termine toda resposta com exatamente um bloco, sem nada depois dele.
188
+
189
+ Para chamar uma ferramenta:
190
+
191
+ ${MARCA_INICIO}
192
+ {"tool_call": {"name": "write_file", "arguments": {"path": "index.html", "content": "..."}}}
193
+ ${MARCA_FIM}
194
+
195
+ Para terminar a tarefa (sem mais ferramenta pra chamar):
196
+
197
+ ${MARCA_INICIO}
198
+ {"assistant_message": "frase curta do que foi feito"}
199
+ ${MARCA_FIM}
200
+
201
+ Nunca omita o bloco. Nunca escreva mais de um bloco por resposta.`;
202
+ }
203
+
204
+ const SKILL_DIR = () => path.join(os.homedir(), '.claude', 'skills', 'primocode-engine');
205
+ const SKILL_PATH = () => path.join(SKILL_DIR(), 'SKILL.md');
206
+
207
+ /** Grava (ou regrava) a skill em ~/.claude/skills/primocode-engine/SKILL.md.
208
+ * Sempre reescreve com o texto atual — barato, e garante que uma versão
209
+ * nova do PrimoCode (com ferramenta nova em catalogo.js) atualiza a skill
210
+ * sozinha na próxima vez que /claude ligar. */
211
+ function garantirSkill() {
212
+ const dir = SKILL_DIR();
213
+ fs.mkdirSync(dir, { recursive: true });
214
+ const caminho = SKILL_PATH();
215
+ fs.writeFileSync(caminho, textoBootstrap(), 'utf-8');
216
+ return { ok: true, caminho };
217
+ }
218
+
219
+ /** executor(cmd) roda um comando e devolve a saída, ou lança. Default:
220
+ * execSync de verdade. Testes injetam uma função fake — sem mock framework. */
221
+ function temClaudeCode(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
222
+ try { executor('claude --version'); return true; }
223
+ catch { return false; }
224
+ }
225
+
226
+ function temOrca(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
227
+ try { executor('orca --version'); return true; }
228
+ catch { return false; }
229
+ }
230
+
231
+ /** executor aqui devolve TEXTO (o JSON cru de `orca status --json`), não
232
+ * só "roda ou lança" — por isso um default diferente do de cima. */
233
+ function orcaAlcancavel(executor = (cmd) => execSync(cmd, { encoding: 'utf-8' })) {
234
+ try {
235
+ const saida = executor('orca status --json');
236
+ const j = JSON.parse(saida);
237
+ return !!(j && j.result && j.result.runtime && j.result.runtime.reachable);
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+
243
+ /** Função pura — nenhum I/O aqui, só decide. */
244
+ function escolherCaminho({ temClaude, temOrcaCli, orcaOk }) {
245
+ if (!temClaude) return 'sem-claude';
246
+ if (temOrcaCli && orcaOk) return 'A';
247
+ return 'B';
248
+ }
249
+
250
+ // Handle do terminal ORCA desta execução do processo — sessão contínua
251
+ // enquanto o CLI ficar de pé. Cursor de leitura por handle, pra "terminal
252
+ // read --cursor" trazer só o que é novo desde o turno anterior.
253
+ let sessao = { handle: null, cursor: '0', bootstrapEnviado: false };
254
+
255
+ function rodarOrca(args, opts = {}) {
256
+ return execFileSync('orca', args,
257
+ { encoding: 'utf-8', timeout: opts.timeout || 15000, ...SEM_APARECER });
258
+ }
259
+
260
+ let contadorId = 0;
261
+ /** Id sintético pro tool_call — o bloco marcado do Claude Code não traz um
262
+ * (só existe UM tool_call por resposta, pela própria regra do bootstrap),
263
+ * mas lib/act.js precisa de tc.id pra parear com a mensagem role:'tool' que
264
+ * vem depois. Único dentro do processo já basta. */
265
+ function proximoId() {
266
+ contadorId += 1;
267
+ return `claude-engine-${contadorId}`;
268
+ }
269
+
270
+ // Caminho A (sessão contínua via ORCA) tem um problema arquitetural real,
271
+ // achado rodando de verdade na verificação manual (Task 9 do plano): o
272
+ // Claude Code interativo desenha a TUI em tela alternada, e
273
+ // `orca terminal read` (scrollback linha a linha) nunca enxerga esse
274
+ // conteúdo — o tail fica preso no eco do comando de lançamento. `orca
275
+ // terminal show`'s preview mostra a tela renderizada de verdade, mas é só
276
+ // um recorte das últimas linhas visíveis, sem garantia de conter o bloco
277
+ // marcado inteiro numa resposta mais longa que o painel. Até esse
278
+ // mecanismo de leitura ser resolvido, o Caminho B (testado de ponta a
279
+ // ponta, funciona) é o único confiável — força ele sempre, mesmo com ORCA
280
+ // disponível. O código do Caminho A fica pronto pra quando isso for
281
+ // resolvido, só não é escolhido.
282
+ const CAMINHO_A_HABILITADO = false;
283
+
284
+ async function garantirEngine() {
285
+ // execFileSync('claude', …) no caminho headless não resolve o shim
286
+ // .cmd/.bat do Windows sem shell:true — quebraria em EVERY turno, mesmo
287
+ // com a detecção acima (que usa execSync com shell) passando. Recusa
288
+ // antes de instalar a skill: honesto e seguro, em vez de "sucesso" que
289
+ // quebra na primeira mensagem real.
290
+ if (process.platform === 'win32') {
291
+ return {
292
+ ok: false,
293
+ error: 'O motor Claude Code ainda não funciona no Windows.',
294
+ comoResolver: 'Aguarde uma próxima versão, ou escolha outra opção no /api.',
295
+ };
296
+ }
297
+ if (!temClaudeCode()) {
298
+ return {
299
+ ok: false,
300
+ error: 'Claude Code não está instalado nesta máquina.',
301
+ comoResolver: 'Instale o Claude Code (veja https://docs.claude.com/claude-code) e escolha Claude Code no /api de novo.',
302
+ };
303
+ }
304
+ garantirSkill();
305
+ const orcaCli = CAMINHO_A_HABILITADO && temOrca();
306
+ const caminho = escolherCaminho({
307
+ temClaude: true,
308
+ temOrcaCli: orcaCli,
309
+ orcaOk: orcaCli ? orcaAlcancavel() : false,
310
+ });
311
+ return { ok: true, caminho };
312
+ }
313
+
314
+ function abrirTerminalOrca() {
315
+ // --dangerously-skip-permissions aqui não dá poder novo ao Claude Code:
316
+ // a skill (textoBootstrap) já instrui ele a nunca usar suas próprias
317
+ // ferramentas nativas. A flag só pula os PRÓPRIOS prompts de permissão
318
+ // dele, que travariam a sessão sem ninguém pra responder.
319
+ const saida = rodarOrca(['terminal', 'create', '--command', 'claude --dangerously-skip-permissions', '--title', 'PrimoCode', '--json']);
320
+ const r = JSON.parse(saida);
321
+ // `orca terminal create --json` aninha o handle em result.terminal.handle,
322
+ // não em result.handle — confirmado rodando o comando de verdade (Task 9).
323
+ return r.result.terminal.handle;
324
+ }
325
+
326
+ function esperarIdleOrca(handle, timeoutMs) {
327
+ rodarOrca(['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', String(timeoutMs)], { timeout: timeoutMs + 5000 });
328
+ }
329
+
330
+ function ultimaMensagem(messages) {
331
+ const m = (messages || [])[messages.length - 1];
332
+ return m && m.content ? String(m.content) : '';
333
+ }
334
+
335
+ /** Devolve a decisão pro onEvent — tool_call, ou assistant_message (com
336
+ * degradação pra texto puro quando o bloco marcado não veio, em vez de
337
+ * travar o turno em silêncio). */
338
+ /* ─────────────────────────────────────────────── o que não deu para ler ──
339
+ O motor às vezes devolve algo que este lado não sabe processar: bloco com
340
+ sintaxe torta, ou uma ferramenta que não existe. O usuário via isso na
341
+ tela — "O motor Claude Code devolveu uma decisão que não consegui ler" —
342
+ e a tarefa parava.
343
+
344
+ Erro de forma não é assunto do usuário. Quem errou a forma foi o motor, e
345
+ quem sabe consertar é ele. Então o problema volta PARA ELE, dizendo
346
+ exatamente o que estava errado, e ele reescreve a ação. Só depois de
347
+ algumas tentativas seguidas é que alguém precisa saber.
348
+
349
+ `pedirCorrecao` é preenchido por quem chamou (o caminho headless ou o
350
+ ORCA), porque só eles sabem como falar com o motor de novo. */
351
+ function diagnosticar(cru, nomesValidos) {
352
+ if (cru.includes(MARCA_INICIO) && !cru.includes(MARCA_FIM)) {
353
+ return 'você abriu ' + MARCA_INICIO + ' e nunca fechou com ' + MARCA_FIM + '.';
354
+ }
355
+ if (!cru.includes(MARCA_INICIO) && cru.includes(MARCA_FIM)) {
356
+ return 'apareceu ' + MARCA_FIM + ' sem o ' + MARCA_INICIO + ' antes.';
357
+ }
358
+ const dentro = /<<<PRIMOCODE>>>([\s\S]*?)<<<FIM>>>/.exec(cru);
359
+ if (dentro) {
360
+ try {
361
+ JSON.parse(dentro[1].trim());
362
+ return 'o JSON até é válido, mas não tem "tool_call" nem "assistant_message" na raiz.';
363
+ } catch (e) {
364
+ return 'o conteúdo entre as marcas não é JSON válido: ' + e.message
365
+ + '. Lembre de escapar quebra de linha como \\n dentro das strings.';
366
+ }
367
+ }
368
+ if (nomesValidos) return 'a ferramenta pedida não existe. As que existem: ' + nomesValidos + '.';
369
+ return 'não achei um bloco marcado na sua resposta.';
370
+ }
371
+
372
+ function textoDeCorrecao(cru, motivo) {
373
+ return 'A sua última resposta não pôde ser executada: ' + motivo
374
+ + '\n\nO que você mandou foi:\n---\n' + String(cru).slice(0, 1200) + '\n---\n\n'
375
+ + 'Reescreva AGORA a mesma intenção numa ação que o sistema reconheça. '
376
+ + 'Responda com exatamente UM bloco:\n'
377
+ + MARCA_INICIO + '\n{"tool_call":{"name":"<uma ferramenta do catálogo>","arguments":{...}}}\n' + MARCA_FIM
378
+ + '\nou, se era só texto para a pessoa, ' + MARCA_INICIO
379
+ + '\n{"assistant_message":"..."}\n' + MARCA_FIM
380
+ + '\nNada fora do bloco.';
381
+ }
382
+
383
+ const MAX_CORRECOES = 3;
384
+
385
+ async function emitirDecisao(textoLido, onEvent, pedirCorrecao, tentativa, validos) {
386
+ tentativa = tentativa || 0;
387
+ const decisao = extrairDecisao(textoLido);
388
+ if (!decisao) {
389
+ const cru = String(textoLido || '').trim();
390
+ // Traz a marca do protocolo mas o bloco não dá para ler? O motor
391
+ // TENTOU chamar ferramenta e errou a sintaxe. Em vez de mostrar isso
392
+ // ao usuário, devolvemos o problema ao motor com o diagnóstico e
393
+ // pedimos a mesma intenção escrita direito.
394
+ if (cru.includes(MARCA_INICIO) || cru.includes(MARCA_FIM)) {
395
+ if (pedirCorrecao && tentativa < MAX_CORRECOES) {
396
+ const motivo = diagnosticar(cru);
397
+ try {
398
+ const novo = await pedirCorrecao(textoDeCorrecao(cru, motivo));
399
+ return await emitirDecisao(novo, onEvent, pedirCorrecao, tentativa + 1, validos);
400
+ } catch (e) {
401
+ // A correção em si falhou (motor fora do ar, tempo
402
+ // esgotado). Cai no caminho antigo, que o loop repete.
403
+ }
404
+ }
405
+ onEvent({ erro: 'Failed to call a function: o motor Claude devolveu um bloco ilegível — repetindo a rodada.' });
406
+ return;
407
+ }
408
+ // Sem marca nenhuma: é resposta em texto, e texto é resposta válida.
409
+ onEvent({ assistant_message: { content: cru } });
410
+ return;
411
+ }
412
+ if (decisao.tool_call) {
413
+ // Ferramenta que não existe é o outro jeito de o motor devolver algo
414
+ // que este lado não sabe processar. Antes viraria uma ação falhada
415
+ // com "ferramenta desconhecida" — ruído para o usuário por um erro
416
+ // que é do motor. Volta para ele, com a lista do que existe.
417
+ const nome = decisao.tool_call.name;
418
+ if (validos && validos.length && validos.indexOf(nome) === -1) {
419
+ if (pedirCorrecao && tentativa < MAX_CORRECOES) {
420
+ try {
421
+ const novo = await pedirCorrecao(textoDeCorrecao(
422
+ JSON.stringify(decisao),
423
+ 'a ferramenta "' + nome + '" não existe. As que existem: ' + validos.join(', ') + '.'));
424
+ return await emitirDecisao(novo, onEvent, pedirCorrecao, tentativa + 1, validos);
425
+ } catch (e) { /* motor mudo: segue e deixa o loop tratar */ }
426
+ }
427
+ onEvent({ erro: 'Failed to call a function: ferramenta "' + nome + '" não existe — repetindo a rodada.' });
428
+ return;
429
+ }
430
+ // lib/act.js usa tc.id pra parear a mensagem role:'tool' com este
431
+ // tool_call (messages.push({ tool_call_id: tc.id, ... })) — sem id
432
+ // aqui, esse pareamento quebra.
433
+ onEvent({ tool_call: { id: proximoId(), name: nome, arguments: decisao.tool_call.arguments || {} } });
434
+ }
435
+ else if (decisao.assistant_message) {
436
+ // O modelo pode confundir a forma com a do tool_call (que É um
437
+ // objeto) e emitir {"assistant_message": {"content": "..."}} em vez
438
+ // de string pura. Sem essa coerção, `content` vira um objeto
439
+ // aninhado e lib/act.js quebra com TypeError no `.trim()` — a
440
+ // travessia direta que este arquivo inteiro existe pra evitar.
441
+ const am = decisao.assistant_message;
442
+ const conteudo = typeof am === 'string'
443
+ ? am
444
+ : (am && typeof am.content === 'string' ? am.content : JSON.stringify(am));
445
+ onEvent({ assistant_message: { content: conteudo } });
446
+ }
447
+ else onEvent({ assistant_message: { content: JSON.stringify(decisao) } });
448
+ }
449
+
450
+ async function requestActViaOrca(payload, onEvent) {
451
+ if (!sessao.handle) {
452
+ try {
453
+ sessao.handle = abrirTerminalOrca();
454
+ sessao.cursor = '0';
455
+ sessao.bootstrapEnviado = false;
456
+ esperarIdleOrca(sessao.handle, 30000);
457
+ } catch (e) {
458
+ // Mesmo tratamento do try/catch abaixo: se o terminal não
459
+ // chegou a ficar pronto (timeout do tui-idle, por exemplo), o
460
+ // handle nunca foi confirmado usável — zera pra próxima
461
+ // chamada abrir um terminal novo do zero, em vez de deixar
462
+ // sessao.handle setado com algo que nunca funcionou.
463
+ sessao.handle = null;
464
+ throw e;
465
+ }
466
+ }
467
+ let texto = ultimaMensagem(payload.messages);
468
+ if (!sessao.bootstrapEnviado) {
469
+ texto = 'Use a skill primocode-engine para esta sessão inteira — ela está em ~/.claude/skills/primocode-engine/SKILL.md.\n\n' + texto;
470
+ sessao.bootstrapEnviado = true;
471
+ }
472
+ try {
473
+ rodarOrca(['terminal', 'send', '--terminal', sessao.handle, '--text', texto, '--enter']);
474
+ esperarIdleOrca(sessao.handle, 120000);
475
+ const lida = rodarOrca(['terminal', 'read', '--terminal', sessao.handle, '--cursor', sessao.cursor, '--json']);
476
+ const leitura = JSON.parse(lida);
477
+ // Mesma correção do handle: `terminal read --json` também aninha em
478
+ // result.terminal — e o texto vem como ARRAY de linhas (`tail`), não
479
+ // uma string única (`text`). Confirmado rodando de verdade (Task 9).
480
+ const t = leitura.result.terminal || {};
481
+ sessao.cursor = t.nextCursor != null ? String(t.nextCursor) : sessao.cursor;
482
+ // No terminal a correção é só mais uma mensagem na MESMA sessão: o
483
+ // motor já tem o protocolo e a conversa toda em contexto, então
484
+ // basta dizer o que saiu errado.
485
+ const pedirCorrecao = (aviso) => {
486
+ rodarOrca(['terminal', 'send', '--terminal', sessao.handle, '--text', aviso, '--enter']);
487
+ esperarIdleOrca(sessao.handle, 120000);
488
+ const outra = JSON.parse(rodarOrca(['terminal', 'read', '--terminal', sessao.handle,
489
+ '--cursor', sessao.cursor, '--json']));
490
+ const t2 = outra.result.terminal || {};
491
+ sessao.cursor = t2.nextCursor != null ? String(t2.nextCursor) : sessao.cursor;
492
+ return (t2.tail || []).join('\n');
493
+ };
494
+ await emitirDecisao((t.tail || []).join('\n'), onEvent, pedirCorrecao, 0, nomesDasTools(payload));
495
+ } catch (e) {
496
+ // Handle obsoleto (terminal fechado, ORCA reiniciou) — documentado
497
+ // pelo próprio `orca agent-context` como terminal_handle_stale. Zera
498
+ // a sessão: a PRÓXIMA chamada abre um terminal novo do zero. Esta
499
+ // chamada ainda falha (o usuário vê o erro e pode só pedir de novo).
500
+ sessao.handle = null;
501
+ throw e;
502
+ }
503
+ }
504
+
505
+ /** Sem sessão viva (caminho B), o histórico inteiro vira texto a cada
506
+ * chamada — mas `content` sozinho perde a mensagem quando é um turno de
507
+ * tool_calls (content vem null ali, o que importa está em tool_calls) ou
508
+ * uma resposta de ferramenta (role:'tool'). Reconstrói os dois casos em
509
+ * vez de deixar "assistant: null" no texto. */
510
+ function linhaDoHistorico(m) {
511
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
512
+ const chamadas = m.tool_calls
513
+ .map((tc) => `chamou ${tc.function.name} com ${tc.function.arguments}`)
514
+ .join('; ');
515
+ return `assistant: ${chamadas}`;
516
+ }
517
+ if (m.role === 'tool') return `resultado da ferramenta (${m.tool_call_id}): ${m.content}`;
518
+ return `${m.role}: ${m.content}`;
519
+ }
520
+
521
+ /**
522
+ * O protocolo INTEIRO, mais a conversa, num prompt só.
523
+ *
524
+ * A versão anterior dizia "use a skill primocode-engine" e confiava que o
525
+ * headless a carregaria — quando não carregava, o Claude respondia sem bloco
526
+ * nenhum e o turno morria. Prompt maior, mas o motor tem contexto de sobra e
527
+ * o formato SEMPRE chega.
528
+ *
529
+ * SEM o cabeçalho YAML da skill: ele começa com "---", e um prompt que começa
530
+ * com traço o parser de argumentos lê como OPÇÃO — "error: unknown option
531
+ * '---'" na cara do usuário, com o protocolo inteiro despejado no terminal.
532
+ * Foi exatamente o que aconteceu.
533
+ *
534
+ * Isto é o que o /claude e o /codex têm em comum, e é de propósito que more
535
+ * num lugar só: são dois motores, e tem de ser UM protocolo.
536
+ */
537
+ function corpoDoProtocolo() {
538
+ return textoBootstrap().replace(/^---[\s\S]*?---\s*/, '');
539
+ }
540
+
541
+ /** O pedido de conserto: o protocolo de novo, mais o que saiu errado. Sem a
542
+ * conversa — o assunto aqui é a FORMA, e repetir o histórico inteiro só
543
+ * gastaria contexto no que não é o problema. */
544
+ function promptDeCorrecao(aviso) {
545
+ return corpoDoProtocolo() + '\n\n' + aviso;
546
+ }
547
+
548
+ function promptDoProtocolo(payload) {
549
+ const historico = (payload.messages || []).map(linhaDoHistorico).join('\n\n');
550
+ return corpoDoProtocolo()
551
+ + '\n\nIMPORTANTE: no JSON do bloco, escreva quebras de linha como \\n (escapadas), nunca linha de verdade.'
552
+ + '\n\n# A conversa até aqui\n\n' + historico
553
+ + '\n\nResponda com exatamente UM bloco marcado.';
554
+ }
555
+
556
+ async function requestActViaHeadless(payload, onEvent) {
557
+ const corpoBootstrap = corpoDoProtocolo();
558
+ const prompt = promptDoProtocolo(payload);
559
+ // --dangerously-skip-permissions: mesma razão do caminho ORCA acima —
560
+ // a skill já proíbe o Claude Code de usar suas próprias ferramentas
561
+ // nativas; a flag só evita que ele pare pedindo permissão pra si mesmo
562
+ // numa chamada headless sem ninguém pra responder.
563
+ // O prompt vai por STDIN, nunca por argumento: argumento tem limite de
564
+ // tamanho (a conversa cresce) e qualquer conteúdo começando com traço
565
+ // seria lido como opção. stdin não tem nenhum dos dois problemas.
566
+ const saida = execFileSync('claude', [
567
+ '-p',
568
+ '--output-format', 'json',
569
+ '--dangerously-skip-permissions',
570
+ ...argsDoModelo(),
571
+ ], { encoding: 'utf-8', timeout: 180000, input: prompt, maxBuffer: 32 * 1024 * 1024,
572
+ env: process.env, ...SEM_APARECER });
573
+ const r = JSON.parse(saida);
574
+ // A correção é uma segunda ida ao motor, com o mesmo protocolo e o
575
+ // diagnóstico do que saiu errado. Barata: o prompt é curto, e evita que
576
+ // um erro de sintaxe do motor vire erro na cara do usuário.
577
+ const pedirCorrecao = (aviso) => {
578
+ const s = execFileSync('claude', [
579
+ '-p', '--output-format', 'json', '--dangerously-skip-permissions',
580
+ ...argsDoModelo(),
581
+ ], {
582
+ encoding: 'utf-8', timeout: 120000, maxBuffer: 32 * 1024 * 1024,
583
+ env: process.env,
584
+ input: promptDeCorrecao(aviso),
585
+ ...SEM_APARECER,
586
+ });
587
+ return JSON.parse(s).result || '';
588
+ };
589
+ await emitirDecisao(r.result || '', onEvent, pedirCorrecao, 0, nomesDasTools(payload));
590
+ }
591
+
592
+ /** Os nomes que o sistema realmente sabe executar, tirados do payload. */
593
+ function nomesDasTools(payload) {
594
+ return ((payload && payload.tools) || [])
595
+ .map((t) => (t && t.function && t.function.name) || t.name)
596
+ .filter(Boolean);
597
+ }
598
+
599
+ /** Mesma forma de api.requestAct(server, payload, onEvent) — `server` não
600
+ * é usado aqui (não existe servidor neste motor), fica só pra manter a
601
+ * assinatura uniforme com o outro lado da escolha em lib/act.js. */
602
+ async function requestAct(server, payload, onEvent) {
603
+ try {
604
+ const engineInfo = await garantirEngine();
605
+ if (!engineInfo.ok) {
606
+ onEvent({ erro: `${engineInfo.error} ${engineInfo.comoResolver || ''}`.trim() });
607
+ return;
608
+ }
609
+ if (engineInfo.caminho === 'A') await requestActViaOrca(payload, onEvent);
610
+ else await requestActViaHeadless(payload, onEvent);
611
+ } catch (e) {
612
+ onEvent({ erro: e.message });
613
+ }
614
+ }
615
+
616
+ module.exports = {
617
+ MARCA_INICIO, MARCA_FIM, extrairDecisao,
618
+ catalogoParaTexto, textoBootstrap,
619
+ // O protocolo sai daqui para o lib/codex-engine.js usar o MESMO. Ele é o
620
+ // que faz um motor de fora virar cérebro do PrimoCode: o texto que
621
+ // ensina o formato, a leitura do bloco marcado e a volta para consertar
622
+ // o que veio torto. Reimplementar aquilo no outro motor seria manter dois
623
+ // protocolos que divergem na primeira correção de um deles.
624
+ linhaDoHistorico, nomesDasTools, promptDoProtocolo, promptDeCorrecao,
625
+ MODELO_CLAUDE, argsDoModelo,
626
+ SKILL_DIR, SKILL_PATH, garantirSkill,
627
+ temClaudeCode, temOrca, orcaAlcancavel, escolherCaminho,
628
+ garantirEngine, requestAct, emitirDecisao,
629
+ _emitirDecisao: emitirDecisao,
630
+ };
@@ -0,0 +1,207 @@
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
+ /* O MOTOR EXTERNO NÃO APARECE — a mesma regra do claude-engine, e o mesmo
36
+ comentário longo mora lá. Em resumo: `execFileSync` sem `stdio` HERDA o
37
+ stderr, e um aviso do Codex cairia no meio da tela do PrimoCode com a cara de
38
+ outro produto. Capturado, ele some da tela e `e.stderr` passa a existir — o
39
+ `catch` daqui já lia esse campo e estava lendo `null` desde sempre. */
40
+ const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
41
+
42
+ const claude = require('./claude-engine');
43
+
44
+ /**
45
+ * O modelo barato do Codex.
46
+ *
47
+ * O /claude vai no Opus porque ali o que se quer é a decisão mais certa. Aqui
48
+ * o pedido foi o contrário — "o modelo baratinho" — e faz sentido: quem liga
49
+ * o /codex está usando a franquia da própria conta, e o motor do PrimoCode dá
50
+ * muitas voltas por tarefa.
51
+ *
52
+ * Vazio manda sem `--model`, e vale o padrão da conta. E se o Codex recusar
53
+ * este modelo, o motor tenta de novo SEM ele em vez de morrer — ver
54
+ * `chamarCodex`.
55
+ */
56
+ const MODELO_CODEX = process.env.PRIMOCODE_CODEX_MODELO !== undefined
57
+ ? process.env.PRIMOCODE_CODEX_MODELO
58
+ : 'gpt-5-mini';
59
+
60
+ /** O programa. Trocável para o teste pôr um de mentira no lugar. */
61
+ const CODEX = process.env.PRIMOCODE_CODEX_BIN || 'codex';
62
+
63
+ const TEMPO = 180000;
64
+ const BUFFER = 32 * 1024 * 1024;
65
+
66
+ function temCodex(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
67
+ try { executor(`${CODEX} --version`); return true; }
68
+ catch { return false; }
69
+ }
70
+
71
+ /**
72
+ * O que o `codex exec` desta máquina aceita.
73
+ *
74
+ * Passar uma opção que a versão instalada não conhece não dá "opção
75
+ * desconhecida" e pronto: dá uma saída de ajuda no lugar da resposta, e o
76
+ * motor devolveria "não consegui ler a decisão" em toda rodada — com a
77
+ * pessoa sem nenhuma pista de que o problema é uma flag.
78
+ *
79
+ * Perguntar ao `--help` custa uma chamada por sessão e responde com o que
80
+ * ESTA versão faz, em vez do que a versão de hoje fazia quando isto foi
81
+ * escrito.
82
+ */
83
+ let _ajuda = null;
84
+ function ajudaDoExec(executor) {
85
+ if (_ajuda !== null) return _ajuda;
86
+ const rodar = executor || ((args) => execFileSync(CODEX, args,
87
+ { encoding: 'utf-8', timeout: 20000, ...SEM_APARECER }));
88
+ try { _ajuda = String(rodar(['exec', '--help']) || ''); }
89
+ catch { _ajuda = ''; } // sem ajuda: segue com o mínimo
90
+ return _ajuda;
91
+ }
92
+
93
+ function esquecerAjuda() { _ajuda = null; }
94
+
95
+ /**
96
+ * Os argumentos da chamada.
97
+ *
98
+ * `--skip-git-repo-check` só entra se esta versão o conhecer: sem ele, o
99
+ * Codex se recusa a rodar fora de um repositório git — e o PrimoCode trabalha
100
+ * em pasta comum o tempo todo.
101
+ */
102
+ function argsDoExec({ comModelo = true, ajuda = null } = {}) {
103
+ const h = ajuda === null ? ajudaDoExec() : ajuda;
104
+ const args = ['exec'];
105
+ if (h.includes('--skip-git-repo-check')) args.push('--skip-git-repo-check');
106
+ // O Codex pede confirmação para agir na máquina. Aqui ele não age: o
107
+ // protocolo proíbe ferramenta nativa, quem executa é o PrimoCode. A opção
108
+ // só evita que ele pare esperando uma resposta que ninguém vai dar.
109
+ if (h.includes('--dangerously-bypass-approvals-and-sandbox')) {
110
+ args.push('--dangerously-bypass-approvals-and-sandbox');
111
+ } else if (h.includes('--full-auto')) {
112
+ args.push('--full-auto');
113
+ }
114
+ if (comModelo && MODELO_CODEX) args.push('--model', MODELO_CODEX);
115
+ // O prompt vai por STDIN: por argumento ele teria limite de tamanho (a
116
+ // conversa cresce) e qualquer texto começando com traço seria lido como
117
+ // opção. `-` é como o exec pede para ler da entrada.
118
+ args.push('-');
119
+ return args;
120
+ }
121
+
122
+ /** Um modelo que a conta não tem é o erro mais provável desta chamada — e o
123
+ * único que dá para consertar sozinho. */
124
+ function recusouOModelo(texto) {
125
+ return /model|modelo/i.test(texto)
126
+ && /not (found|available|supported)|unknown|invalid|does not|inexistente|inválid/i.test(texto);
127
+ }
128
+
129
+ /**
130
+ * Chama o Codex e devolve o texto. Se ele recusar o modelo, tenta UMA vez sem
131
+ * `--model` e avisa — cair para o padrão da conta é melhor que a tarefa
132
+ * morrer por causa de um nome que mudou de catálogo.
133
+ */
134
+ function chamarCodex(prompt, { executor, avisar = () => {} } = {}) {
135
+ // A chave própria da pessoa, quando ela configurou uma. Sem configuração
136
+ // o ambiente vai INTACTO — passar a variável vazia apagaria a chave que
137
+ // ela já tem exportada no shell, e o Codex pararia de funcionar por ter
138
+ // sido "configurado".
139
+ const rodar = executor || ((args, entrada) => execFileSync(CODEX, args, {
140
+ encoding: 'utf-8', timeout: TEMPO, input: entrada, maxBuffer: BUFFER,
141
+ env: process.env, ...SEM_APARECER,
142
+ }));
143
+ try {
144
+ return rodar(argsDoExec(), prompt);
145
+ } catch (e) {
146
+ const detalhe = `${e.message || ''}\n${(e.stderr || '')}\n${(e.stdout || '')}`;
147
+ if (MODELO_CODEX && recusouOModelo(detalhe)) {
148
+ avisar(`o Codex não aceitou o modelo ${MODELO_CODEX} — indo com o padrão da sua conta`);
149
+ return rodar(argsDoExec({ comModelo: false }), prompt);
150
+ }
151
+ throw new Error(primeiraLinhaUtil(detalhe) || e.message);
152
+ }
153
+ }
154
+
155
+ /** A primeira linha que diz alguma coisa. O erro do Codex costuma vir com
156
+ * banner, versão e caminho antes do motivo, e jogar tudo isso na tela é
157
+ * esconder o motivo no meio. */
158
+ function primeiraLinhaUtil(texto) {
159
+ for (const linha of String(texto || '').split('\n')) {
160
+ const l = linha.trim();
161
+ if (l && !/^(Command failed|codex\b|\s*$)/i.test(l)) return l.slice(0, 300);
162
+ }
163
+ return '';
164
+ }
165
+
166
+ function garantirEngine() {
167
+ if (!temCodex()) {
168
+ return {
169
+ ok: false,
170
+ error: 'O Codex não está instalado nesta máquina.',
171
+ comoResolver: 'Instale o Codex CLI da OpenAI, entre na sua conta com `codex login`, '
172
+ + 'e escolha Codex no /api de novo.',
173
+ };
174
+ }
175
+ return { ok: true, modelo: MODELO_CODEX || '(o padrão da sua conta)' };
176
+ }
177
+
178
+ /** Mesma forma de api.requestAct(server, payload, onEvent) — é isso que deixa
179
+ * o lib/act.js trocar de motor sem saber de nada disto. */
180
+ async function requestAct(server, payload, onEvent) {
181
+ try {
182
+ const pronto = garantirEngine();
183
+ if (!pronto.ok) {
184
+ onEvent({ erro: `${pronto.error} ${pronto.comoResolver || ''}`.trim() });
185
+ return;
186
+ }
187
+ const prompt = claude.promptDoProtocolo(payload);
188
+ const saida = chamarCodex(prompt, { avisar: (m) => onEvent({ aviso: m }) });
189
+
190
+ // A correção é uma segunda ida ao motor, com o mesmo protocolo e o
191
+ // diagnóstico do que saiu errado. Barata, e evita que um erro de forma
192
+ // do motor vire erro na cara de quem pediu a tarefa.
193
+ const pedirCorrecao = (aviso) => chamarCodex(claude.promptDeCorrecao(aviso));
194
+
195
+ await claude.emitirDecisao(saida, onEvent, pedirCorrecao, 0,
196
+ claude.nomesDasTools(payload));
197
+ } catch (e) {
198
+ onEvent({ erro: e.message });
199
+ }
200
+ }
201
+
202
+ module.exports = {
203
+ MODELO_CODEX, CODEX,
204
+ temCodex, garantirEngine, requestAct,
205
+ argsDoExec, ajudaDoExec, esquecerAjuda,
206
+ chamarCodex, recusouOModelo, primeiraLinhaUtil,
207
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * motor-cli.js — o Claude Code ou o Codex da pessoa como cérebro do PrimoCode.
3
+ *
4
+ * "Coloca o /claude de novo, mas dentro do /api: aí coloca /claude e
5
+ * /codex." (24/09/2026)
6
+ *
7
+ * Sem chave: usa a assinatura que a pessoa já tem nessas ferramentas. O
8
+ * agente (o loop, as ferramentas, a tela) continua sendo o do PrimoCode; a
9
+ * ferramenta de fora só decide o próximo passo, pelo protocolo de bloco
10
+ * marcado de lib/claude-engine.js (o Codex usa o mesmo).
11
+ *
12
+ * Os motores só sabem o `requestAct`. Chat e resumo, aqui, são uma pergunta
13
+ * de texto simples ao mesmo CLI; o plano não divide (uma ida a mais a um CLI
14
+ * lento custaria mais do que a divisão poupa) e segue em passo único.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const { execFileSync } = require('child_process');
20
+ const cerebro = require('./cerebro.json');
21
+
22
+ const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
23
+
24
+ function motorDe(id) {
25
+ if (id === 'codex') {
26
+ /* O padrão antigo do motor (gpt-5-mini) é recusado por conta de
27
+ assinatura do ChatGPT — medido: "o Codex não aceitou o modelo", e
28
+ uma segunda chamada em TODA rodada. Pelo /api, vai o modelo da
29
+ conta. Quem quiser outro ainda usa PRIMOCODE_CODEX_MODELO. */
30
+ if (process.env.PRIMOCODE_CODEX_MODELO === undefined) process.env.PRIMOCODE_CODEX_MODELO = '';
31
+ return require('../codex-engine.js');
32
+ }
33
+ return require('../claude-engine.js');
34
+ }
35
+
36
+ /** Uma pergunta de texto, resposta de texto. Sem protocolo, sem ferramenta. */
37
+ function perguntar(id, texto) {
38
+ if (id === 'codex') return String(motorDe('codex').chamarCodex(texto) || '').trim();
39
+ const claude = motorDe('claude-code');
40
+ const saida = execFileSync('claude', ['-p', '--output-format', 'json', ...claude.argsDoModelo()], {
41
+ encoding: 'utf-8', timeout: 120000, input: texto, maxBuffer: 8 * 1024 * 1024, ...SEM_APARECER,
42
+ });
43
+ try { return String(JSON.parse(saida).result || '').trim(); } catch { return String(saida || '').trim(); }
44
+ }
45
+
46
+ function criar(id) {
47
+ const motor = motorDe(id);
48
+ return {
49
+ requestAct: (servidor, payload, emit) => motor.requestAct(servidor, payload, emit),
50
+
51
+ async requestChat(_servidor, { messages, prompt, history } = {}, emit) {
52
+ const turnos = Array.isArray(messages) ? messages
53
+ : [...(Array.isArray(history) ? history : []), ...(prompt ? [{ role: 'user', content: prompt }] : [])];
54
+ const conversa = turnos.filter((m) => m && m.role && typeof m.content === 'string')
55
+ .map((m) => `${m.role === 'user' ? 'PESSOA' : 'VOCÊ'}: ${m.content}`).join('\n');
56
+ try {
57
+ const resposta = perguntar(id, `${cerebro.SYSTEM_CHAT}\n\nResponda só à última fala da PESSOA.\n\n${conversa}`);
58
+ if (resposta) emit({ delta: resposta });
59
+ } catch (e) {
60
+ emit({ erro: e.message });
61
+ }
62
+ emit({ done: true });
63
+ },
64
+
65
+ async requestResumo(_servidor, { resumo, mensagens } = {}) {
66
+ const novas = (mensagens || []).filter((m) => m && m.role);
67
+ if (!novas.length) return { resumo: String(resumo || '') };
68
+ const corpo = (resumo ? `RESUMO ANTERIOR:\n${resumo}\n\n` : '') + 'MENSAGENS NOVAS:\n'
69
+ + novas.map((m) => `${m.role === 'user' ? 'USUÁRIO' : m.role === 'assistant' ? 'AGENTE' : 'FERRAMENTA'}: `
70
+ + String(typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')).slice(0, 1200)).join('\n');
71
+ try {
72
+ return { resumo: perguntar(id, `${cerebro.SYSTEM_RESUMO}\n\n${corpo}`) || String(resumo || '') };
73
+ } catch (e) {
74
+ return { resumo: String(resumo || ''), erro: e.message };
75
+ }
76
+ },
77
+
78
+ async requestPlano(_servidor, { prompt } = {}) {
79
+ return { passos: [{ titulo: 'tarefa', pedido: String(prompt || '').trim() }], dividiu: false };
80
+ },
81
+ };
82
+ }
83
+
84
+ /** A ferramenta está instalada? (sem rodar nada caro) */
85
+ function instalado(id) {
86
+ if (id === 'codex') return motorDe('codex').temCodex();
87
+ return motorDe('claude-code').temClaudeCode();
88
+ }
89
+
90
+ module.exports = { criar, instalado, perguntar };
@@ -33,6 +33,22 @@ const ARQUIVO = () => path.join(os.homedir(), '.primocode', 'provedor.json');
33
33
  /* A ordem é a do menu. `modelos` é a preferência: ao colar a chave, fica o
34
34
  primeiro que a conta da pessoa de fato tem (a lista vem da própria API). */
35
35
  const PROVEDORES = {
36
+ /* Sem chave: a assinatura que a pessoa já tem na ferramenta de fora, que
37
+ roda escondida como cérebro (lib/local/motor-cli.js). */
38
+ 'claude-code': {
39
+ nome: 'Claude Code',
40
+ cli: true,
41
+ modelos: ['opus'],
42
+ onde: 'https://docs.claude.com/claude-code',
43
+ dica: 'sua assinatura do Claude, sem chave',
44
+ },
45
+ codex: {
46
+ nome: 'Codex',
47
+ cli: true,
48
+ modelos: [''],
49
+ onde: 'https://github.com/openai/codex',
50
+ dica: 'sua assinatura do ChatGPT, sem chave',
51
+ },
36
52
  gemini: {
37
53
  nome: 'Gemini (Google)',
38
54
  base: 'https://generativelanguage.googleapis.com/v1beta/openai',
@@ -96,7 +112,9 @@ function atual() {
96
112
  try {
97
113
  const j = JSON.parse(fs.readFileSync(ARQUIVO(), 'utf8'));
98
114
  const p = PROVEDORES[j.provedor];
99
- cache = p && j.chave ? { id: j.provedor, nome: p.nome, base: j.base || p.base, modelo: j.modelo || p.modelos[0], chave: j.chave } : null;
115
+ cache = p && (j.chave || p.cli)
116
+ ? { id: j.provedor, nome: p.nome, cli: !!p.cli, base: j.base || p.base, modelo: j.modelo || p.modelos[0], chave: j.chave || '' }
117
+ : null;
100
118
  } catch { cache = null; }
101
119
  cacheMtime = st.mtimeMs;
102
120
  return cache;
@@ -106,7 +124,7 @@ function salvar({ provedor, chave, modelo }) {
106
124
  if (!PROVEDORES[provedor]) throw new Error(`provedor desconhecido: ${provedor}`);
107
125
  const arq = ARQUIVO();
108
126
  fs.mkdirSync(path.dirname(arq), { recursive: true });
109
- fs.writeFileSync(arq, JSON.stringify({ provedor, chave: String(chave).trim(), modelo }, null, 2), { mode: 0o600 });
127
+ fs.writeFileSync(arq, JSON.stringify({ provedor, chave: String(chave || '').trim(), modelo }, null, 2), { mode: 0o600 });
110
128
  try { fs.chmodSync(arq, 0o600); } catch { /* Windows */ }
111
129
  cache = null;
112
130
  return atual();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0-beta.1",
3
+ "version": "9.8.0-beta.2",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -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/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"