primocode 9.8.0-beta.0 → 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 +5 -4
- package/bin/primocode.js +73 -0
- package/lib/api.js +21 -0
- package/lib/claude-engine.js +630 -0
- package/lib/codex-engine.js +207 -0
- package/lib/local/cerebro.json +2116 -0
- package/lib/local/entrada.js +332 -0
- package/lib/local/motor-cli.js +90 -0
- package/lib/local/motor.js +264 -0
- package/lib/local/provedor.js +169 -0
- package/package.json +2 -2
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motor.js — o agente falando DIRETO com a IA da pessoa, daqui da máquina.
|
|
3
|
+
*
|
|
4
|
+
* Faz o que o primocode-server faz nas rotas /api/act/stream, /api/chat/stream,
|
|
5
|
+
* /api/resumir e /api/plano, com o mesmo prompt e as mesmas ferramentas
|
|
6
|
+
* (lib/local/cerebro.json, copiado do servidor por scripts/cerebro-do-servidor.js),
|
|
7
|
+
* e devolve os MESMOS eventos. Para o loop do agente (lib/act.js), nada muda:
|
|
8
|
+
* ele não sabe se quem respondeu foi o servidor ou a API da pessoa.
|
|
9
|
+
*
|
|
10
|
+
* O que fica de fora de propósito: a fila de provedores e o orçamento de 6.000
|
|
11
|
+
* tokens por minuto. Os dois existem para caber na cota compartilhada — aqui
|
|
12
|
+
* a conta é da pessoa, e o teto é o dela.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const cerebro = require('./cerebro.json');
|
|
18
|
+
const entrada = require('./entrada.js');
|
|
19
|
+
const provedor = require('./provedor.js');
|
|
20
|
+
const catalogo = require('../catalogo.js');
|
|
21
|
+
|
|
22
|
+
// A conversa pode ocupar isto, em tokens, além do prompt e das ferramentas.
|
|
23
|
+
// Folgado: os modelos de hoje têm 128 mil ou mais de contexto.
|
|
24
|
+
const TETO_CONVERSA = Number(process.env.PRIMOCODE_TETO_LOCAL || 60000);
|
|
25
|
+
const SAIDA_MAXIMA = 8192;
|
|
26
|
+
const ESFORCO_PESQUISA = ['high', 'xhigh', 'max', 'ultracode'];
|
|
27
|
+
|
|
28
|
+
/* ── AS FERRAMENTAS QUE ESTE CLIENTE EXECUTA ─────────────────────────────
|
|
29
|
+
* O servidor ainda oferece o Estúdio antigo (studio_*), que este cliente não
|
|
30
|
+
* executa mais; e o cliente tem o Estúdio novo (estudio_*, planejar_producao),
|
|
31
|
+
* que o servidor não conhece. Local, a lista certa é a do CLIENTE: o schema
|
|
32
|
+
* vem do servidor quando existe lá, e do catálogo quando só existe aqui. */
|
|
33
|
+
function ferramentasDoCliente() {
|
|
34
|
+
const doServidor = new Map(cerebro.TOOLS.map((t) => [t.function.name, t]));
|
|
35
|
+
const lista = [];
|
|
36
|
+
for (const grupo of catalogo.GRUPOS) {
|
|
37
|
+
for (const item of grupo.itens) {
|
|
38
|
+
if (doServidor.has(item.tool)) { lista.push(doServidor.get(item.tool)); continue; }
|
|
39
|
+
lista.push({ type: 'function', function: {
|
|
40
|
+
name: item.tool,
|
|
41
|
+
description: `${item.faz}. Argumentos: ${item.args || 'nenhum'}`.slice(0, 1800),
|
|
42
|
+
parameters: { type: 'object', properties: {}, additionalProperties: true },
|
|
43
|
+
} });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!lista.some((t) => t.function.name === 'finish') && doServidor.has('finish')) lista.push(doServidor.get('finish'));
|
|
47
|
+
return lista;
|
|
48
|
+
}
|
|
49
|
+
let FERRAMENTAS = null;
|
|
50
|
+
let FERRAMENTAS_SUB = null;
|
|
51
|
+
function ferramentas(subagent) {
|
|
52
|
+
if (!FERRAMENTAS) {
|
|
53
|
+
FERRAMENTAS = ferramentasDoCliente();
|
|
54
|
+
FERRAMENTAS_SUB = FERRAMENTAS.filter((t) => !['spawn_agent', 'construir_app'].includes(t.function.name));
|
|
55
|
+
}
|
|
56
|
+
return subagent ? FERRAMENTAS_SUB : FERRAMENTAS;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** O erro da API, no formato que o loop já sabe tratar (429 espera, 401 avisa). */
|
|
60
|
+
async function erroHttp(r, p) {
|
|
61
|
+
let msg = '';
|
|
62
|
+
try {
|
|
63
|
+
const corpo = await r.text();
|
|
64
|
+
try {
|
|
65
|
+
const j = JSON.parse(corpo);
|
|
66
|
+
const e = Array.isArray(j) ? j[0] && j[0].error : j.error;
|
|
67
|
+
msg = (e && (e.message || e.status)) || j.message || corpo;
|
|
68
|
+
} catch { msg = corpo; }
|
|
69
|
+
} catch { /* sem corpo */ }
|
|
70
|
+
const quem = p ? p.nome : 'o provedor';
|
|
71
|
+
if (r.status === 401 || r.status === 403) {
|
|
72
|
+
return new Error(`${r.status} ${quem} recusou a chave. Rode /api para colar outra, ou /api off para voltar ao servidor.`);
|
|
73
|
+
}
|
|
74
|
+
const espera = r.headers && r.headers.get && r.headers.get('retry-after');
|
|
75
|
+
return new Error(`${r.status} ${quem}: ${String(msg).slice(0, 300)}${espera ? ` Please try again in ${espera}s.` : ''}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cabecalhos(p) {
|
|
79
|
+
const h = { 'Content-Type': 'application/json', Authorization: `Bearer ${p.chave}` };
|
|
80
|
+
if (p.id === 'openrouter') { h['HTTP-Referer'] = 'https://github.com/parisgroup-ai/primocode'; h['X-Title'] = 'PrimoCode'; }
|
|
81
|
+
return h;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Uma chamada inteira, sem stream (resumo, plano). */
|
|
85
|
+
async function completar(messages, { maxTokens = 600, temperatura = 0.2 } = {}) {
|
|
86
|
+
const p = provedor.atual();
|
|
87
|
+
const r = await fetch(`${p.base}/chat/completions`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: cabecalhos(p),
|
|
90
|
+
body: JSON.stringify({ model: p.modelo, messages, temperature: temperatura, max_tokens: maxTokens }),
|
|
91
|
+
signal: AbortSignal.timeout(90000),
|
|
92
|
+
});
|
|
93
|
+
if (!r.ok) throw await erroHttp(r, p);
|
|
94
|
+
const j = await r.json();
|
|
95
|
+
return String((j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content) || '').trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Lê o stream SSE do /chat/completions. Chama `aoPedaco(delta, fim)` a cada
|
|
100
|
+
* pedaço. Resolve quando acaba ou quando `sinal` corta.
|
|
101
|
+
*/
|
|
102
|
+
async function lerStream(corpo, { sinal, aoPedaco }) {
|
|
103
|
+
const p = provedor.atual();
|
|
104
|
+
let r;
|
|
105
|
+
try {
|
|
106
|
+
r = await fetch(`${p.base}/chat/completions`, {
|
|
107
|
+
method: 'POST', headers: cabecalhos(p), body: JSON.stringify({ ...corpo, model: p.modelo, stream: true }), signal: sinal,
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (sinal && sinal.aborted) return;
|
|
111
|
+
throw new Error(`ECONNRESET não consegui falar com ${p.nome}: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
if (!r.ok) throw await erroHttp(r, p);
|
|
114
|
+
const decod = new TextDecoder();
|
|
115
|
+
let buffer = '';
|
|
116
|
+
try {
|
|
117
|
+
for await (const pedaco of r.body) {
|
|
118
|
+
buffer += decod.decode(pedaco, { stream: true });
|
|
119
|
+
const linhas = buffer.split('\n');
|
|
120
|
+
buffer = linhas.pop();
|
|
121
|
+
for (const linha of linhas) {
|
|
122
|
+
const l = linha.trim();
|
|
123
|
+
if (!l.startsWith('data:')) continue;
|
|
124
|
+
const dado = l.slice(5).trim();
|
|
125
|
+
if (dado === '[DONE]') return;
|
|
126
|
+
let j;
|
|
127
|
+
try { j = JSON.parse(dado); } catch { continue; }
|
|
128
|
+
if (j.error) throw new Error(`${j.error.code || 400} ${j.error.message || JSON.stringify(j.error)}`);
|
|
129
|
+
const escolha = j.choices && j.choices[0];
|
|
130
|
+
if (escolha) aoPedaco(escolha.delta || {}, escolha.finish_reason);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (sinal && sinal.aborted) return;
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Junta os pedaços de tool_call. Gemini às vezes manda sem `index` e sem `id`. */
|
|
140
|
+
function juntarChamadas(acumuladas, pedacos) {
|
|
141
|
+
for (const tc of pedacos || []) {
|
|
142
|
+
let idx;
|
|
143
|
+
if (Number.isInteger(tc.index)) idx = tc.index;
|
|
144
|
+
else if (tc.id) {
|
|
145
|
+
idx = acumuladas.findIndex((a) => a && a.id === tc.id);
|
|
146
|
+
if (idx < 0) idx = acumuladas.length; // id novo: chamada nova
|
|
147
|
+
} else idx = Math.max(0, acumuladas.length - 1); // sem nada: continua a última
|
|
148
|
+
if (!acumuladas[idx]) acumuladas[idx] = { id: tc.id || '', name: '', arguments: '' };
|
|
149
|
+
const alvo = acumuladas[idx];
|
|
150
|
+
if (tc.id) alvo.id = tc.id;
|
|
151
|
+
if (tc.function && tc.function.name) alvo.name = tc.function.name;
|
|
152
|
+
if (tc.function && tc.function.arguments) {
|
|
153
|
+
const a = tc.function.arguments;
|
|
154
|
+
alvo.arguments += typeof a === 'string' ? a : JSON.stringify(a);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return acumuladas;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── /api/act/stream ─────────────────────────────────────────────────── */
|
|
161
|
+
async function requestAct(_servidor, body, emit) {
|
|
162
|
+
const { messages: convo = [], prompt, history, effort, projectDir, projectName, subagent, sinal } = body || {};
|
|
163
|
+
const pedido = [...convo].reverse().find((m) => m && m.role === 'user');
|
|
164
|
+
const texto = String((pedido && pedido.content) || prompt || '');
|
|
165
|
+
const forcar = effort && ESFORCO_PESQUISA.includes(String(effort)) ? ['pesquisa'] : [];
|
|
166
|
+
|
|
167
|
+
// Todas, sempre: o recorte por assunto do servidor existe para caber em
|
|
168
|
+
// 6.000 tokens por minuto, e ainda não conhece o Estúdio novo (estudio_*)
|
|
169
|
+
// — com ele, um pedido de vídeo chegaria sem a ferramenta de vídeo.
|
|
170
|
+
const tools = ferramentas(subagent);
|
|
171
|
+
const sistema = subagent
|
|
172
|
+
? cerebro.SYSTEM_SUB
|
|
173
|
+
: entrada.montarPrompt(cerebro.SYSTEM_NUCLEO, cerebro.SECOES, texto, { forcar, jaAprendeu: entrada.jaAprendeu(convo) });
|
|
174
|
+
const messages = [{ role: 'system', content: sistema }];
|
|
175
|
+
if (projectDir) {
|
|
176
|
+
messages.push({ role: 'system', content: `[Projeto: ${projectName || 'sem nome'}]\n[Pasta: ${projectDir}]\n`
|
|
177
|
+
+ 'Todo caminho que você passar para write_file/read_file/list_dir é relativo a esta pasta. '
|
|
178
|
+
+ 'Use caminhos relativos simples (ex: "index.html", "src/app.js"). Nunca use caminho absoluto.' });
|
|
179
|
+
}
|
|
180
|
+
const conversa = convo.length ? convo : [...(history || []), { role: 'user', content: String(prompt || '') }];
|
|
181
|
+
messages.push(...entrada.caberNoTeto(conversa.filter((m) => m && m.role), TETO_CONVERSA).messages);
|
|
182
|
+
|
|
183
|
+
let texto_ = '';
|
|
184
|
+
const chamadas = [];
|
|
185
|
+
await lerStream({ messages, tools, tool_choice: 'auto', temperature: 0.1, max_tokens: SAIDA_MAXIMA }, {
|
|
186
|
+
sinal,
|
|
187
|
+
aoPedaco: (delta) => {
|
|
188
|
+
if (delta.tool_calls) juntarChamadas(chamadas, delta.tool_calls);
|
|
189
|
+
if (delta.content && !chamadas.length) { texto_ += delta.content; emit({ delta: delta.content }); }
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
chamadas.forEach((tc, i) => {
|
|
193
|
+
if (!tc || !tc.name) return;
|
|
194
|
+
let args = {};
|
|
195
|
+
try { args = JSON.parse(tc.arguments || '{}'); } catch { /* o loop pede de novo */ }
|
|
196
|
+
emit({ tool_call: { id: tc.id || `local_${Date.now()}_${i}`, name: tc.name, arguments: args } });
|
|
197
|
+
});
|
|
198
|
+
if (texto_) emit({ assistant_message: { content: texto_ } });
|
|
199
|
+
emit({ done: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ── /api/chat/stream ────────────────────────────────────────────────── */
|
|
203
|
+
async function requestChat(_servidor, body, emit) {
|
|
204
|
+
const { messages, prompt, history, sinal } = body || {};
|
|
205
|
+
const turnos = Array.isArray(messages) ? messages
|
|
206
|
+
: [...(Array.isArray(history) ? history : []), ...(prompt ? [{ role: 'user', content: prompt }] : [])];
|
|
207
|
+
await lerStream({
|
|
208
|
+
messages: [{ role: 'system', content: cerebro.SYSTEM_CHAT }, ...turnos.filter((m) => m && m.role)],
|
|
209
|
+
temperature: 0.4, max_tokens: 1200,
|
|
210
|
+
}, { sinal, aoPedaco: (delta) => { if (delta.content) emit({ delta: delta.content }); } });
|
|
211
|
+
emit({ done: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ── /api/resumir ────────────────────────────────────────────────────── */
|
|
215
|
+
async function requestResumo(_servidor, { resumo, mensagens } = {}) {
|
|
216
|
+
const novas = Array.isArray(mensagens) ? mensagens.filter((m) => m && m.role) : [];
|
|
217
|
+
if (!novas.length) return { resumo: String(resumo || '') };
|
|
218
|
+
const virarTexto = (m) => {
|
|
219
|
+
const quem = m.role === 'user' ? 'USUÁRIO' : m.role === 'assistant' ? 'AGENTE' : 'FERRAMENTA';
|
|
220
|
+
const conteudo = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
|
|
221
|
+
return `${quem}: ${conteudo.slice(0, 1200)}`;
|
|
222
|
+
};
|
|
223
|
+
const corpo = (resumo ? `RESUMO ANTERIOR:\n${resumo}\n\n` : '') + `MENSAGENS NOVAS:\n${novas.map(virarTexto).join('\n')}`;
|
|
224
|
+
try {
|
|
225
|
+
const limpo = await completar([{ role: 'system', content: cerebro.SYSTEM_RESUMO }, { role: 'user', content: corpo }], { maxTokens: 600 });
|
|
226
|
+
return { resumo: limpo || String(resumo || '') };
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return { resumo: String(resumo || ''), erro: e.message };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Os passos do plano, tolerando cerca de markdown. Mesma regra do servidor. */
|
|
233
|
+
function lerPassos(texto) {
|
|
234
|
+
const bruto = String(texto || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');
|
|
235
|
+
const abre = bruto.indexOf('{');
|
|
236
|
+
const fecha = bruto.lastIndexOf('}');
|
|
237
|
+
if (abre === -1 || fecha <= abre) return null;
|
|
238
|
+
let j;
|
|
239
|
+
try { j = JSON.parse(bruto.slice(abre, fecha + 1)); } catch { return null; }
|
|
240
|
+
const lista = Array.isArray(j && j.passos) ? j.passos : null;
|
|
241
|
+
if (!lista || !lista.length) return null;
|
|
242
|
+
const limpos = lista
|
|
243
|
+
.map((p) => ({ titulo: String((p && p.titulo) || 'passo').slice(0, 60), pedido: String((p && p.pedido) || '').trim() }))
|
|
244
|
+
.filter((p) => p.pedido.length > 10)
|
|
245
|
+
.slice(0, 3);
|
|
246
|
+
return limpos.length ? limpos : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* ── /api/plano ──────────────────────────────────────────────────────── */
|
|
250
|
+
async function requestPlano(_servidor, { prompt } = {}) {
|
|
251
|
+
const cru = String(prompt || '').trim();
|
|
252
|
+
const passoUnico = [{ titulo: 'tarefa', pedido: cru }];
|
|
253
|
+
if (cru.length < 25) return { passos: passoUnico, dividiu: false };
|
|
254
|
+
try {
|
|
255
|
+
const passos = lerPassos(await completar(
|
|
256
|
+
[{ role: 'system', content: cerebro.SYSTEM_PLANO }, { role: 'user', content: cru }],
|
|
257
|
+
{ maxTokens: 800, temperatura: 0.1 })) || passoUnico;
|
|
258
|
+
return { passos, dividiu: passos.length > 1 };
|
|
259
|
+
} catch {
|
|
260
|
+
return { passos: passoUnico, dividiu: false };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = { requestAct, requestChat, requestResumo, requestPlano, juntarChamadas, lerPassos, ferramentas, ativo: () => !!provedor.atual() };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provedor.js — a chave da própria pessoa, e com qual IA ela fala.
|
|
3
|
+
*
|
|
4
|
+
* "Fica batendo no limite. Coloca um comando /provider onde você seleciona
|
|
5
|
+
* se é Gemini, ChatGPT etc., cola a key e ela roda local com a key
|
|
6
|
+
* configurada." (23/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* Com um provedor configurado (/api), o agente fala DIRETO com a API dele, daqui da
|
|
9
|
+
* máquina (lib/local/motor.js). O servidor e a cota compartilhada saem do
|
|
10
|
+
* caminho: o limite passa a ser o da conta da pessoa.
|
|
11
|
+
*
|
|
12
|
+
* ── ONDE A CHAVE MORA ────────────────────────────────────────────────────
|
|
13
|
+
* Em `~/.primocode/provedor.json`, permissão 600. Não no config.json: esse é
|
|
14
|
+
* o arquivo que alguém abre para conferir um ajuste ou cola num chamado.
|
|
15
|
+
*
|
|
16
|
+
* ── O QUE NUNCA APARECE ──────────────────────────────────────────────────
|
|
17
|
+
* A chave inteira — nem ao listar, nem num erro. Mostra-se o começo e o fim,
|
|
18
|
+
* o bastante para reconhecer qual é.
|
|
19
|
+
*
|
|
20
|
+
* Todos falam o formato de chat da OpenAI (/chat/completions, tools, stream):
|
|
21
|
+
* é o que Gemini, Claude, Groq, OpenRouter e DeepSeek aceitam também. Um
|
|
22
|
+
* cliente só, e o provedor é só o endereço, o modelo e a chave.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const ARQUIVO = () => path.join(os.homedir(), '.primocode', 'provedor.json');
|
|
32
|
+
|
|
33
|
+
/* A ordem é a do menu. `modelos` é a preferência: ao colar a chave, fica o
|
|
34
|
+
primeiro que a conta da pessoa de fato tem (a lista vem da própria API). */
|
|
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
|
+
},
|
|
52
|
+
gemini: {
|
|
53
|
+
nome: 'Gemini (Google)',
|
|
54
|
+
base: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
55
|
+
modelos: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'],
|
|
56
|
+
onde: 'https://aistudio.google.com/apikey',
|
|
57
|
+
dica: 'tem plano grátis',
|
|
58
|
+
},
|
|
59
|
+
chatgpt: {
|
|
60
|
+
nome: 'ChatGPT (OpenAI)',
|
|
61
|
+
base: 'https://api.openai.com/v1',
|
|
62
|
+
modelos: ['gpt-5-mini', 'gpt-5', 'gpt-4.1-mini', 'gpt-4o-mini'],
|
|
63
|
+
onde: 'https://platform.openai.com/api-keys',
|
|
64
|
+
dica: 'pago por uso',
|
|
65
|
+
},
|
|
66
|
+
claude: {
|
|
67
|
+
nome: 'Claude (Anthropic)',
|
|
68
|
+
base: 'https://api.anthropic.com/v1',
|
|
69
|
+
modelos: ['claude-sonnet-5', 'claude-haiku-4-5-20251001', 'claude-opus-5-5'],
|
|
70
|
+
onde: 'https://console.anthropic.com/settings/keys',
|
|
71
|
+
dica: 'pago por uso',
|
|
72
|
+
},
|
|
73
|
+
groq: {
|
|
74
|
+
nome: 'Groq',
|
|
75
|
+
base: 'https://api.groq.com/openai/v1',
|
|
76
|
+
modelos: ['openai/gpt-oss-120b', 'llama-3.3-70b-versatile', 'openai/gpt-oss-20b'],
|
|
77
|
+
onde: 'https://console.groq.com/keys',
|
|
78
|
+
dica: 'tem plano grátis, muito rápido',
|
|
79
|
+
},
|
|
80
|
+
openrouter: {
|
|
81
|
+
nome: 'OpenRouter',
|
|
82
|
+
base: 'https://openrouter.ai/api/v1',
|
|
83
|
+
modelos: ['openai/gpt-oss-120b', 'google/gemini-2.5-flash', 'anthropic/claude-sonnet-5'],
|
|
84
|
+
onde: 'https://openrouter.ai/keys',
|
|
85
|
+
dica: 'vários modelos numa chave só',
|
|
86
|
+
},
|
|
87
|
+
deepseek: {
|
|
88
|
+
nome: 'DeepSeek',
|
|
89
|
+
base: 'https://api.deepseek.com/v1',
|
|
90
|
+
modelos: ['deepseek-chat'],
|
|
91
|
+
onde: 'https://platform.deepseek.com/api_keys',
|
|
92
|
+
dica: 'barato',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** A chave, irreconhecível para quem olha por cima do ombro. */
|
|
97
|
+
function mascarar(chave) {
|
|
98
|
+
const k = String(chave || '');
|
|
99
|
+
if (k.length <= 10) return '••••';
|
|
100
|
+
return `${k.slice(0, 4)}…${k.slice(-4)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let cache = null;
|
|
104
|
+
let cacheMtime = 0;
|
|
105
|
+
|
|
106
|
+
/** O provedor configurado, ou null (= segue pelo servidor, como sempre). */
|
|
107
|
+
function atual() {
|
|
108
|
+
if (process.env.PRIMOCODE_SEM_PROVEDOR === '1') return null;
|
|
109
|
+
let st;
|
|
110
|
+
try { st = fs.statSync(ARQUIVO()); } catch { cache = null; return null; }
|
|
111
|
+
if (cache && st.mtimeMs === cacheMtime) return cache;
|
|
112
|
+
try {
|
|
113
|
+
const j = JSON.parse(fs.readFileSync(ARQUIVO(), 'utf8'));
|
|
114
|
+
const p = PROVEDORES[j.provedor];
|
|
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;
|
|
118
|
+
} catch { cache = null; }
|
|
119
|
+
cacheMtime = st.mtimeMs;
|
|
120
|
+
return cache;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function salvar({ provedor, chave, modelo }) {
|
|
124
|
+
if (!PROVEDORES[provedor]) throw new Error(`provedor desconhecido: ${provedor}`);
|
|
125
|
+
const arq = ARQUIVO();
|
|
126
|
+
fs.mkdirSync(path.dirname(arq), { recursive: true });
|
|
127
|
+
fs.writeFileSync(arq, JSON.stringify({ provedor, chave: String(chave || '').trim(), modelo }, null, 2), { mode: 0o600 });
|
|
128
|
+
try { fs.chmodSync(arq, 0o600); } catch { /* Windows */ }
|
|
129
|
+
cache = null;
|
|
130
|
+
return atual();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function trocarModelo(modelo) {
|
|
134
|
+
const a = atual();
|
|
135
|
+
if (!a) return null;
|
|
136
|
+
return salvar({ provedor: a.id, chave: a.chave, modelo });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function desligar() {
|
|
140
|
+
try { fs.unlinkSync(ARQUIVO()); } catch { /* já não havia */ }
|
|
141
|
+
cache = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A chave colada serve? Pergunta a lista de modelos à própria API. */
|
|
145
|
+
async function conferir(provedor, chave, { buscar = fetch } = {}) {
|
|
146
|
+
const p = PROVEDORES[provedor];
|
|
147
|
+
let r;
|
|
148
|
+
try {
|
|
149
|
+
r = await buscar(`${p.base}/models`, {
|
|
150
|
+
headers: { Authorization: `Bearer ${String(chave).trim()}` },
|
|
151
|
+
signal: AbortSignal.timeout(15000),
|
|
152
|
+
});
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return { ok: false, error: `não consegui falar com ${p.nome}: ${e.message}` };
|
|
155
|
+
}
|
|
156
|
+
if (r.status === 401 || r.status === 403) return { ok: false, error: `${p.nome} recusou a chave (${r.status}). Confira se copiou inteira.` };
|
|
157
|
+
if (!r.ok) return { ok: false, error: `${p.nome} respondeu ${r.status} ao conferir a chave.` };
|
|
158
|
+
let lista = [];
|
|
159
|
+
try {
|
|
160
|
+
const j = await r.json();
|
|
161
|
+
lista = (j.data || j.models || []).map((m) => String(m.id || m.name || '').replace(/^models\//, '')).filter(Boolean);
|
|
162
|
+
} catch { /* sem lista: fica a preferência */ }
|
|
163
|
+
// Nenhum da preferência na lista (ou lista vazia): fica o primeiro dela, e
|
|
164
|
+
// `/api modelo <nome>` troca.
|
|
165
|
+
const modelo = p.modelos.find((m) => lista.includes(m)) || p.modelos[0];
|
|
166
|
+
return { ok: true, modelo, disponiveis: lista };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { PROVEDORES, ARQUIVO, mascarar, atual, salvar, trocarModelo, desligar, conferir };
|
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/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"
|