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 +3 -3
- package/bin/primocode.js +16 -3
- package/lib/api.js +5 -2
- package/lib/claude-engine.js +630 -0
- package/lib/codex-engine.js +207 -0
- package/lib/local/motor-cli.js +90 -0
- package/lib/local/provedor.js +20 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.8.0-beta.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
210
|
-
|
|
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 };
|
package/lib/local/provedor.js
CHANGED
|
@@ -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
|
|
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.
|
|
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"
|