primocode 9.8.0-beta.1 → 9.8.0-beta.3
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 +21 -6
- package/bin/primocode.js +338 -56
- package/comandos/explicar.md +14 -0
- package/comandos/publicar.md +10 -0
- package/comandos/revisar.md +13 -0
- package/kits/landing/index.html +162 -0
- package/kits/landing/script.js +199 -0
- package/kits/landing/style.css +219 -0
- package/lib/act.js +43 -10
- package/lib/api.js +5 -2
- package/lib/checkpoint.js +134 -0
- package/lib/claude-engine.js +630 -0
- package/lib/codex-engine.js +207 -0
- package/lib/comandos.js +104 -0
- package/lib/config.js +18 -4
- package/lib/kits.js +81 -0
- package/lib/local/motor-cli.js +90 -0
- package/lib/local/provedor.js +20 -2
- package/lib/mencoes.js +98 -0
- package/lib/perigo.js +256 -0
- package/package.json +4 -2
|
@@ -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
|
+
};
|
package/lib/comandos.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* comandos.js — o pedido que a pessoa repete vira um comando dela.
|
|
3
|
+
*
|
|
4
|
+
* Quem pede "revisa o projeto e me explica os bugs sem jargão" toda semana
|
|
5
|
+
* não deveria digitar isso toda semana. Um arquivo `revisar.md` com o pedido
|
|
6
|
+
* dentro vira `/revisar`, e `/revisar o login` troca `$ARGUMENTOS` por
|
|
7
|
+
* "o login". É o mesmo desenho dos comandos do Claude Code.
|
|
8
|
+
*
|
|
9
|
+
* ── POR QUE NÃO É UMA SKILL ──────────────────────────────────────────────
|
|
10
|
+
* Skill entra sozinha quando o pedido casa com o `quando:` dela. Comando só
|
|
11
|
+
* entra quando a pessoa chama pelo nome. Um é conhecimento de fundo, o outro
|
|
12
|
+
* é um atalho de digitação — e misturar os dois faria um atalho disparar
|
|
13
|
+
* sem ninguém ter pedido.
|
|
14
|
+
*
|
|
15
|
+
* ── ONDE MORAM, E QUEM GANHA ─────────────────────────────────────────────
|
|
16
|
+
* 1. `comandos/` do pacote — os três que vêm de fábrica ("primocode")
|
|
17
|
+
* 2. `~/.primocode/comandos/` — os da pessoa, em qualquer projeto ("global")
|
|
18
|
+
* 3. `<projeto>/.primocode/comandos/` — os daquele projeto ("projeto")
|
|
19
|
+
* O mais perto do trabalho ganha: o do projeto sabe coisas do projeto que o
|
|
20
|
+
* global não sabe. E o de fábrica perde para qualquer um, para a pessoa poder
|
|
21
|
+
* reescrever o `/revisar` do jeito dela sem mexer no pacote.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { separar } = require('./skills.js');
|
|
30
|
+
|
|
31
|
+
const PASTA_DO_PACOTE = path.join(__dirname, '..', 'comandos');
|
|
32
|
+
// O nome vira `/nome` na caixa de digitar: sem espaço, sem acento estranho,
|
|
33
|
+
// sem barra — senão o comando existe no disco e não há como digitá-lo.
|
|
34
|
+
const NOME_VALIDO = /^[a-z0-9][a-z0-9_-]*$/;
|
|
35
|
+
|
|
36
|
+
/** Um arquivo .md → { nome, descricao, corpo, arquivo }. */
|
|
37
|
+
function ler(arquivo) {
|
|
38
|
+
const { cabecalho, corpo } = separar(fs.readFileSync(arquivo, 'utf-8'));
|
|
39
|
+
return {
|
|
40
|
+
nome: path.basename(arquivo, '.md').toLowerCase(),
|
|
41
|
+
descricao: cabecalho.descricao || cabecalho.description || '',
|
|
42
|
+
corpo,
|
|
43
|
+
arquivo,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function daPasta(pasta, origem) {
|
|
48
|
+
let nomes = [];
|
|
49
|
+
try { nomes = fs.readdirSync(pasta); } catch { return []; } // pasta que não existe é lista vazia
|
|
50
|
+
const fora = [];
|
|
51
|
+
for (const n of nomes.filter((x) => x.endsWith('.md')).sort()) {
|
|
52
|
+
try {
|
|
53
|
+
const c = ler(path.join(pasta, n));
|
|
54
|
+
if (NOME_VALIDO.test(c.nome) && c.corpo) fora.push({ ...c, origem });
|
|
55
|
+
} catch { /* um arquivo ilegível não derruba os outros */ }
|
|
56
|
+
}
|
|
57
|
+
return fora;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A lista final, um por nome, já com a precedência aplicada. */
|
|
61
|
+
function carregar({ projectDir, home = os.homedir(), pastaDoPacote = PASTA_DO_PACOTE } = {}) {
|
|
62
|
+
const porNome = new Map();
|
|
63
|
+
// Em ordem crescente de prioridade: quem vem depois sobrescreve.
|
|
64
|
+
const camadas = [
|
|
65
|
+
[pastaDoPacote, 'primocode'],
|
|
66
|
+
[path.join(home, '.primocode', 'comandos'), 'global'],
|
|
67
|
+
];
|
|
68
|
+
if (projectDir) camadas.push([path.join(projectDir, '.primocode', 'comandos'), 'projeto']);
|
|
69
|
+
for (const [pasta, origem] of camadas) {
|
|
70
|
+
for (const c of daPasta(pasta, origem)) porNome.set(c.nome, c);
|
|
71
|
+
}
|
|
72
|
+
return [...porNome.values()].sort((a, b) => a.nome.localeCompare(b.nome));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** O prompt que vai ao agente. */
|
|
76
|
+
function expandir(comando, argumentos) {
|
|
77
|
+
const args = String(argumentos || '').trim();
|
|
78
|
+
const corpo = comando.corpo;
|
|
79
|
+
if (corpo.includes('$ARGUMENTOS')) return corpo.split('$ARGUMENTOS').join(args).trim();
|
|
80
|
+
// Sem a marca, o que foi digitado não pode sumir: vai no fim.
|
|
81
|
+
return args ? `${corpo}\n\n${args}` : corpo;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Cria ~/.primocode/comandos/<nome>.md com um esqueleto. Devolve o caminho. */
|
|
85
|
+
function criar(nome, { home = os.homedir() } = {}) {
|
|
86
|
+
const n = String(nome || '').trim().toLowerCase().replace(/^\//, '');
|
|
87
|
+
if (!NOME_VALIDO.test(n)) return { ok: false, error: 'Nome só com letras minúsculas, números, - e _.' };
|
|
88
|
+
const pasta = path.join(home, '.primocode', 'comandos');
|
|
89
|
+
const arquivo = path.join(pasta, n + '.md');
|
|
90
|
+
if (fs.existsSync(arquivo)) return { ok: false, error: `Já existe: ${arquivo}`, arquivo };
|
|
91
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
92
|
+
fs.writeFileSync(arquivo, [
|
|
93
|
+
'---',
|
|
94
|
+
`descricao: o que o /${n} faz, numa linha`,
|
|
95
|
+
'---',
|
|
96
|
+
'Escreva aqui o pedido, do jeito que você digitaria.',
|
|
97
|
+
'',
|
|
98
|
+
'O que vier depois do comando entra no lugar desta palavra: $ARGUMENTOS',
|
|
99
|
+
'',
|
|
100
|
+
].join('\n'));
|
|
101
|
+
return { ok: true, arquivo };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { PASTA_DO_PACOTE, NOME_VALIDO, ler, carregar, expandir, criar };
|
package/lib/config.js
CHANGED
|
@@ -129,15 +129,25 @@ function listProjects() {
|
|
|
129
129
|
} catch { return []; }
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/* Palavras que dizem o TIPO da coisa, não QUAL coisa. "landing da barbearia"
|
|
133
|
+
e "landing-nova" (de outra empresa) dividiam "landing", e o pedido novo caía
|
|
134
|
+
dentro do projeto antigo — medido em 24/09: a landing da Navalha de Ouro foi
|
|
135
|
+
escrita em landing-nova/. Só o assunto conta como parecido. */
|
|
136
|
+
const GENERICAS = new Set(['landing', 'lading', 'page', 'pagina', 'site', 'app', 'aplicativo', 'projeto',
|
|
137
|
+
'novo', 'nova', 'sistema', 'web', 'html', 'teste', 'versao', 'final', 'the', 'para', 'pra']);
|
|
138
|
+
function tokensDoAssunto(nome) {
|
|
139
|
+
return slugify(nome).split('-').filter((t) => t.length > 2 && !GENERICAS.has(t) && !/^v?\d+$/.test(t));
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
// Projeto parecido com o que o usuário pediu? Compara os pedaços do slug.
|
|
133
143
|
// "jogo-de-carro" vs "jogo-carro-2d" -> compartilham jogo+carro -> 0.5
|
|
134
144
|
function findSimilarProject(projectName, minScore = 0.4) {
|
|
135
|
-
const wanted = new Set(
|
|
145
|
+
const wanted = new Set(tokensDoAssunto(projectName));
|
|
136
146
|
if (!wanted.size) return null;
|
|
137
147
|
|
|
138
148
|
let best = null;
|
|
139
149
|
for (const existing of listProjects()) {
|
|
140
|
-
const tokens = new Set(existing
|
|
150
|
+
const tokens = new Set(tokensDoAssunto(existing));
|
|
141
151
|
if (!tokens.size) continue;
|
|
142
152
|
let shared = 0;
|
|
143
153
|
for (const t of wanted) if (tokens.has(t)) shared++;
|
|
@@ -288,6 +298,9 @@ function loadConfig() {
|
|
|
288
298
|
theme: file.theme || 'primo',
|
|
289
299
|
// Modo escolhido dura entre sessões: quem liga o auto quer o auto amanhã.
|
|
290
300
|
mode: process.env.PRIMOCODE_MODE || file.mode || 'auto',
|
|
301
|
+
// O /permissoes: quando ele pergunta antes de agir. Dura entre sessões
|
|
302
|
+
// pelo mesmo motivo do modo — quem escolheu "perguntar" quer amanhã também.
|
|
303
|
+
permissoes: file.permissoes || 'auto',
|
|
291
304
|
rootDir: ROOT_DIR,
|
|
292
305
|
_file: file,
|
|
293
306
|
};
|
|
@@ -297,8 +310,8 @@ function saveConfig(partial) {
|
|
|
297
310
|
ensureDir(CONFIG_DIR);
|
|
298
311
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
299
312
|
// Só persiste o que faz sentido guardar — nada de campos derivados.
|
|
300
|
-
const { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
|
|
301
|
-
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode };
|
|
313
|
+
const { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode, permissoes } = { ...cur, ...partial };
|
|
314
|
+
const next = { server, model, effort, token, autoApply, currentProject, userName, mascot, theme, mode, permissoes };
|
|
302
315
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
303
316
|
return next;
|
|
304
317
|
}
|
|
@@ -338,6 +351,7 @@ function esquecerProjeto() {
|
|
|
338
351
|
}
|
|
339
352
|
|
|
340
353
|
module.exports = {
|
|
354
|
+
tokensDoAssunto, GENERICAS,
|
|
341
355
|
lembrarProjeto, projetoLembrado, esquecerProjeto, PROJETO_ATIVO_FILE,
|
|
342
356
|
loadConfig, saveConfig, appendHistory,
|
|
343
357
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
package/lib/kits.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kits.js — o modelo grátis começa de uma página pronta, e não do zero.
|
|
3
|
+
*
|
|
4
|
+
* "A IA free fez essa página, ficou boa, mas quero que ela seja mais
|
|
5
|
+
* rápida." · "Como chegar nesse nível com os modelos free? Nem terminou,
|
|
6
|
+
* demora 10 anos." (24/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* ── A CONTA ──────────────────────────────────────────────────────────────
|
|
9
|
+
* Uma landing boa são ~30 KB de HTML+CSS+JS — uns 9 mil tokens de SAÍDA. No
|
|
10
|
+
* plano grátis (6 mil tokens por minuto, entrada e saída juntas) isso não
|
|
11
|
+
* cabe numa resposta: o servidor manda "uma tool por vez", e a página sai em
|
|
12
|
+
* pedaços, com espera a cada minuto. E o modelo fraco, desenhando do zero,
|
|
13
|
+
* erra justamente o que dá o nível: tipografia, espaço, animação.
|
|
14
|
+
*
|
|
15
|
+
* Com o kit, o design já está resolvido (a landing que o Claude fez, com o
|
|
16
|
+
* conteúdo trocado por marcadores). O agente só TROCA texto e três cores com
|
|
17
|
+
* edit_file: ~1–2 mil tokens de saída em vez de 9 mil. Mais rápido E melhor,
|
|
18
|
+
* pela mesma razão — o trabalho difícil já foi feito uma vez, por quem faz
|
|
19
|
+
* bem.
|
|
20
|
+
*
|
|
21
|
+
* Só entra em pasta NOVA (ou vazia): num projeto que já tem página, copiar
|
|
22
|
+
* um kit por cima seria apagar o trabalho da pessoa.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const PASTA_KITS = path.join(__dirname, '..', 'kits');
|
|
31
|
+
|
|
32
|
+
/* O pedido de landing. Estreito de propósito: "crie um app de tarefas" não é
|
|
33
|
+
landing, e um kit errado é pior que kit nenhum. */
|
|
34
|
+
const RE_LANDING = /\b(landing|lading|p[áa]gina (de vendas|de captura|inicial|institucional|de apresenta[çc][ãa]o|pr[ao]|do meu|da minha|da empresa|do neg[óo]cio)|site (institucional|da empresa|do meu|da minha|pr[ao] (minha|meu|a|o))|one ?page|home ?page)\b/i;
|
|
35
|
+
// Pedido que não quer a cara do kit: nada de puxar o kit por cima disso.
|
|
36
|
+
const RE_SEM_KIT = /\b(do zero|sem modelo|sem template|sem kit|react|next(\.?js)?|vue|svelte|angular|tailwind|wordpress)\b/i;
|
|
37
|
+
|
|
38
|
+
const KITS = {
|
|
39
|
+
landing: {
|
|
40
|
+
nome: 'landing',
|
|
41
|
+
arquivos: ['index.html', 'style.css', 'script.js'],
|
|
42
|
+
instrucao: 'A PASTA JÁ TEM UMA LANDING PRONTA (index.html, style.css, script.js), com design profissional, '
|
|
43
|
+
+ 'animações e responsiva. NÃO reescreva os arquivos com write_file: é lento e piora o design. '
|
|
44
|
+
+ 'ADAPTE com edit_file: (1) leia o index.html; (2) troque cada bloco marcado com <!-- TROQUE --> pelo '
|
|
45
|
+
+ 'conteúdo do pedido — textos reais, específicos do negócio, sem inventar fato que você não sabe; '
|
|
46
|
+
+ '(3) no topo do style.css troque só as cores --blue, --orange, --cyan (e as -deep) pelas cores que '
|
|
47
|
+
+ 'combinam com o negócio; (4) apague os comentários TROQUE. Pode remover uma seção que não faz '
|
|
48
|
+
+ 'sentido para o negócio (e o link dela no menu). Não mexa no script.js.',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Qual kit este pedido quer, ou null. */
|
|
53
|
+
function escolher(pedido) {
|
|
54
|
+
const t = String(pedido || '');
|
|
55
|
+
if (!RE_LANDING.test(t) || RE_SEM_KIT.test(t)) return null;
|
|
56
|
+
return KITS.landing;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A pasta está livre para receber o kit? (não existe, ou só tem memory.md) */
|
|
60
|
+
function pastaLivre(dir) {
|
|
61
|
+
try {
|
|
62
|
+
const tem = fs.readdirSync(dir).filter((n) => !n.startsWith('.') && n !== 'memory.md');
|
|
63
|
+
return tem.length === 0;
|
|
64
|
+
} catch { return true; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Copia o kit para `dir`. Devolve os arquivos copiados, ou [] se não copiou. */
|
|
68
|
+
function aplicar(kit, dir) {
|
|
69
|
+
if (!kit || !pastaLivre(dir)) return [];
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
+
const feitos = [];
|
|
72
|
+
for (const nome of kit.arquivos) {
|
|
73
|
+
const destino = path.join(dir, nome);
|
|
74
|
+
if (fs.existsSync(destino)) continue;
|
|
75
|
+
fs.copyFileSync(path.join(PASTA_KITS, kit.nome, nome), destino);
|
|
76
|
+
feitos.push(nome);
|
|
77
|
+
}
|
|
78
|
+
return feitos;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { KITS, PASTA_KITS, escolher, pastaLivre, aplicar };
|
|
@@ -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();
|