primocode 9.7.0 → 9.8.0-beta.1
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 +167 -11
- package/lib/act.js +30 -6
- package/lib/api.js +18 -0
- package/lib/browser.js +80 -6
- package/lib/local/cerebro.json +2116 -0
- package/lib/local/entrada.js +332 -0
- package/lib/local/motor.js +264 -0
- package/lib/local/provedor.js +151 -0
- package/lib/nome-projeto.js +97 -0
- package/lib/repeticao.js +65 -5
- package/lib/tools.js +15 -1
- package/lib/voz.js +7 -1
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +4 -2
- package/voz/primo_voz/transcrever.py +37 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.
|
|
1
|
+
# PrimoCode v9.8.0-beta.1
|
|
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.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.1
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -27,7 +27,7 @@ veja `/mascot` e `/name`.
|
|
|
27
27
|
| **Memória** | Cada projeto lembra o que foi feito, entre sessões | Esquece ao fechar |
|
|
28
28
|
| **Instalação** | Node puro, **zero dependências** — segundos | Pacotes e dependências |
|
|
29
29
|
| **Cara própria** | Mascote, nome e temas que você escolhe | Fixo |
|
|
30
|
-
| **Modelos** | Escolhidos pelo produto
|
|
30
|
+
| **Modelos** | Escolhidos pelo produto (`/effort` regula o esforço), ou a sua própria chave com `/api` | Preso ao provedor do fabricante |
|
|
31
31
|
|
|
32
32
|
Em uma frase: **é o poder de um agente de código no terminal, em português, controlando o seu computador de verdade — sem você precisar configurar chave de API nenhuma.**
|
|
33
33
|
|
|
@@ -287,7 +287,8 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
|
|
|
287
287
|
| `/voice` | Modo voz: uma orb, você fala, ele faz. O cérebro é o mesmo do terminal |
|
|
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
|
-
| `/effort` | O esforço do agente, num slider (←/→, Enter confirma): do `low` ao `ultracode`. O modelo sai daí
|
|
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
292
|
| `/attach <arquivo>` | Anexa arquivo ao próximo pedido |
|
|
292
293
|
| `/detach` · `/files` | Remove / lista anexos |
|
|
293
294
|
| `/all` | Lista as 46 ferramentas que ele sabe usar, por grupo |
|
package/bin/primocode.js
CHANGED
|
@@ -20,6 +20,8 @@ const { escolherModo } = require('../lib/modo.js');
|
|
|
20
20
|
const api = require('../lib/api');
|
|
21
21
|
const tools = require('../lib/tools');
|
|
22
22
|
const cfg = require('../lib/config');
|
|
23
|
+
const nomeProjeto = require('../lib/nome-projeto');
|
|
24
|
+
const provedorLocal = require('../lib/local/provedor');
|
|
23
25
|
const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro } = require('../lib/act');
|
|
24
26
|
const fala = require('../lib/fala');
|
|
25
27
|
const boasVindas = require('../lib/boas-vindas');
|
|
@@ -159,6 +161,7 @@ const SLASH_COMANDOS = [
|
|
|
159
161
|
['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
|
|
160
162
|
['/marketplace', 'a tela das skills: navegue, ligue, crie'],
|
|
161
163
|
['/install-skill', 'instala uma skill de um arquivo'],
|
|
164
|
+
['/api', 'usa a SUA chave (Gemini, ChatGPT, Claude…) direto daqui, sem o limite do servidor'],
|
|
162
165
|
['/status', 'checa o servidor'],
|
|
163
166
|
['/connectors', 'quem está atendendo os seus pedidos'],
|
|
164
167
|
['/name', 'como ele te chama'],
|
|
@@ -171,6 +174,56 @@ const SLASH_COMANDOS = [
|
|
|
171
174
|
['/exit', 'sair'],
|
|
172
175
|
];
|
|
173
176
|
|
|
177
|
+
/* ── /api: a chave da própria pessoa ──────────────────────────────
|
|
178
|
+
* "Fica batendo no limite. Coloca um /provider onde você seleciona se é
|
|
179
|
+
* Gemini, ChatGPT etc., cola a key e ela roda local." (23/09/2026)
|
|
180
|
+
* Com provedor, o agente fala direto com a API da pessoa (lib/local/motor.js);
|
|
181
|
+
* sem, tudo segue pelo servidor como sempre. */
|
|
182
|
+
async function comandoApi(rest) {
|
|
183
|
+
const P = provedorLocal.PROVEDORES;
|
|
184
|
+
const ids = Object.keys(P);
|
|
185
|
+
const arg = String(rest || '').trim();
|
|
186
|
+
const atual = provedorLocal.atual();
|
|
187
|
+
|
|
188
|
+
if (/^(off|desliga[r]?|servidor|nenhum|sair)$/i.test(arg)) {
|
|
189
|
+
provedorLocal.desligar();
|
|
190
|
+
console.log(c.ok(' De volta ao servidor do PrimoCode.') + c.muted(' (a chave foi apagada desta máquina)'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const trocaModelo = arg.match(/^(modelo|model)\s+(\S+)$/i);
|
|
194
|
+
if (trocaModelo) {
|
|
195
|
+
if (!atual) { console.log(c.warn(' Nenhum provedor configurado. Rode /api primeiro.')); return; }
|
|
196
|
+
provedorLocal.trocarModelo(trocaModelo[2]);
|
|
197
|
+
console.log(c.ok(` ${atual.nome} · modelo ${trocaModelo[2]}`));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let id = ids.find((k) => k === arg.toLowerCase() || P[k].nome.toLowerCase().startsWith(arg.toLowerCase()) && arg);
|
|
202
|
+
if (!id) {
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log(' ' + c.bold('Com qual IA você quer rodar?') + c.muted(' a sua chave, direto daqui, sem o limite do servidor'));
|
|
205
|
+
if (atual) console.log(' ' + c.muted('agora: ') + c.white(`${atual.nome} · ${atual.modelo} · chave ${provedorLocal.mascarar(atual.chave)}`));
|
|
206
|
+
ids.forEach((k, i) => console.log(` ${c.brand(String(i + 1))} ${c.white(P[k].nome.padEnd(24))}${c.dim(P[k].dica)}`));
|
|
207
|
+
console.log(` ${c.brand('0')} ${c.white('Servidor do PrimoCode'.padEnd(24))}${c.dim('sem chave, com o limite do plano')}`);
|
|
208
|
+
const escolha = String(await ask(` ${c.brand('?')} Número: `)).trim();
|
|
209
|
+
if (escolha === '0') return comandoApi('off');
|
|
210
|
+
id = ids[Number(escolha) - 1];
|
|
211
|
+
if (!id) { console.log(c.muted(' Nada mudou.')); return; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const p = P[id];
|
|
215
|
+
console.log(c.muted(` Pegue a chave em `) + c.white(p.onde));
|
|
216
|
+
const chave = String(await askSecret(` ${c.brand('?')} Cole a chave do ${p.nome} (não aparece na tela): `)).trim();
|
|
217
|
+
if (!chave) { console.log(c.muted(' Nada mudou.')); return; }
|
|
218
|
+
const sp = new Spinner('conferindo a chave').start();
|
|
219
|
+
const r = await provedorLocal.conferir(id, chave);
|
|
220
|
+
sp.stop();
|
|
221
|
+
if (!r.ok) { console.log(c.warn(` ${r.error}`)); return; }
|
|
222
|
+
const salvo = provedorLocal.salvar({ provedor: id, chave, modelo: r.modelo });
|
|
223
|
+
console.log(c.ok(` Pronto: ${salvo.nome} · ${salvo.modelo}`) + c.muted(` · chave ${provedorLocal.mascarar(chave)}`));
|
|
224
|
+
console.log(c.dim(' Agora os pedidos vão direto daqui, com a sua cota. /api modelo <nome> troca o modelo · /api off volta ao servidor'));
|
|
225
|
+
}
|
|
226
|
+
|
|
174
227
|
// ── /effort: o slider de esforço (estilo Claude Code) ──────────────────
|
|
175
228
|
// Régua com os níveis, o marcador ▲ embaixo do escolhido, "Faster" à
|
|
176
229
|
// esquerda e "Smarter" à direita. ←/→ move, Enter confirma, Esc cancela.
|
|
@@ -563,8 +616,25 @@ function planWorkDir(userText) {
|
|
|
563
616
|
return { dir: cfg.ROOT_DIR, projeto: cfg.getProjectDir(proposto), pending: true, name: proposto };
|
|
564
617
|
}
|
|
565
618
|
|
|
619
|
+
/* O nome da pasta nova vem do modelo (lib/nome-projeto.js), pedido em
|
|
620
|
+
paralelo com o trabalho: a pasta só nasce na primeira escrita, e até lá a
|
|
621
|
+
resposta já chegou. Só pergunta quando o pedido é de CRIAR algo — pedir
|
|
622
|
+
para abrir o Chrome não gasta chamada com nome de pasta. */
|
|
623
|
+
function batizarDepois(plan, userText) {
|
|
624
|
+
if (!plan.pending || !(RE_CRIAR_UM.test(semAcento(userText)) || PEDIDO_DE_CODIGO.test(userText))) return;
|
|
625
|
+
const perguntar = async (texto) => {
|
|
626
|
+
let resposta = '';
|
|
627
|
+
await api.requestChat(state.config.server, {
|
|
628
|
+
prompt: texto, history: [], model: state.config.model, token: state.config.token,
|
|
629
|
+
}, (evt) => { if (evt.delta) resposta += evt.delta; });
|
|
630
|
+
return resposta;
|
|
631
|
+
};
|
|
632
|
+
nomeProjeto.escolher({ pedido: userText, nomes: cfg.listProjects(), perguntar })
|
|
633
|
+
.then((nome) => { if (nome) plan.nomeEscolhido = nome; });
|
|
634
|
+
}
|
|
635
|
+
|
|
566
636
|
function createPendingProject(plan) {
|
|
567
|
-
const dir = cfg.createProjectDir(plan.name);
|
|
637
|
+
const dir = cfg.createProjectDir(plan.nomeEscolhido || plan.name);
|
|
568
638
|
entrarNoProjeto(dir, 'projeto');
|
|
569
639
|
const rotulo = plan.similar ? 'Continuando em' : 'Projeto novo:';
|
|
570
640
|
console.log(c.ok(` ${rotulo} ${state.currentProject}`) + c.muted(` (${dir})`));
|
|
@@ -619,6 +689,7 @@ function prepararPasta(userText) {
|
|
|
619
689
|
talvezTrocarDePasta(userText);
|
|
620
690
|
|
|
621
691
|
const plan = planWorkDir(userText);
|
|
692
|
+
batizarDepois(plan, userText);
|
|
622
693
|
|
|
623
694
|
// Projeto que já existe: entra nele AGORA. Se deixasse para depois, o
|
|
624
695
|
// buildMemoryContext deste turno leria a pasta errada e a memória do projeto
|
|
@@ -914,7 +985,7 @@ async function sendPrompt(userText) {
|
|
|
914
985
|
: '';
|
|
915
986
|
const pedido = passos[i].pedido + contexto + semFatos;
|
|
916
987
|
|
|
917
|
-
const rodarPasso = () => runAct(
|
|
988
|
+
const rodarPasso = (texto = pedido) => runAct(texto, {
|
|
918
989
|
server: state.config.server,
|
|
919
990
|
model: state.config.model,
|
|
920
991
|
effort: state.config.effort,
|
|
@@ -937,15 +1008,38 @@ async function sendPrompt(userText) {
|
|
|
937
1008
|
});
|
|
938
1009
|
|
|
939
1010
|
r = await rodarPasso();
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1011
|
+
/* Passo que parou no meio NÃO devolve a tarefa para a pessoa
|
|
1012
|
+
dividir. Se ele já tinha feito alguma coisa (arquivo, comando),
|
|
1013
|
+
o próprio CLI pede a continuação — com a lista do que já existe
|
|
1014
|
+
no disco, para não refazer. Até CONTINUACOES vezes, e só
|
|
1015
|
+
enquanto cada volta produz algo: continuação que não mexe em
|
|
1016
|
+
nada é laço, e laço para.
|
|
1017
|
+
Num plano de vários passos, o passo que desistiu sem fazer nada
|
|
1018
|
+
ganha UMA segunda chance — costuma ser tropeço transitório do
|
|
1019
|
+
modelo grátis. Falhou de novo: o
|
|
1020
|
+
plano segue com o aviso de "sem fatos", nunca com o texto da
|
|
1021
|
+
falha fantasiado de resultado. */
|
|
1022
|
+
let continuacoes = 0;
|
|
1023
|
+
let tentouDeNovo = false;
|
|
1024
|
+
while (!r.parado && r.concluiu === false) {
|
|
1025
|
+
registro.juntar(livro, r.registro);
|
|
1026
|
+
// Sem registro é erro do provedor (cota, rede): retomar só bate
|
|
1027
|
+
// no mesmo muro.
|
|
1028
|
+
if (r.registro && !registro.vazio(r.registro) && continuacoes < CONTINUACOES) {
|
|
1029
|
+
continuacoes++;
|
|
1030
|
+
console.log(c.dim(' ⎿ continuando de onde parou'));
|
|
1031
|
+
r = await rodarPasso(pedidoDeContinuacao(passos[i].pedido, livro));
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (!tentouDeNovo && passos.length > 1 && registro.vazio(r.registro) && continuacoes === 0) {
|
|
1035
|
+
tentouDeNovo = true;
|
|
1036
|
+
console.log(c.dim(' ⎿ o passo não concluiu — tentando de novo'));
|
|
1037
|
+
r = await rodarPasso();
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
break;
|
|
947
1041
|
}
|
|
948
|
-
registro.juntar(livro, r.registro);
|
|
1042
|
+
if (r.concluiu !== false || r.parado) registro.juntar(livro, r.registro);
|
|
949
1043
|
jaFeito = (r.concluiu !== false)
|
|
950
1044
|
? String((r.assistantMessage && r.assistantMessage.content) || '').slice(0, 1200)
|
|
951
1045
|
: '';
|
|
@@ -1002,6 +1096,18 @@ async function sendPrompt(userText) {
|
|
|
1002
1096
|
const RE_DESCOBRIR = /\b(pesquis|busca|procur|descubr|investig|analis|leia|estud|olh[ae] (o site|a p[áa]gina)|ver o que)\w*/i;
|
|
1003
1097
|
const RE_CRIAR_DEPOIS = /\b(cri[ae]|faz|faça|mont[ae]|ger[ae]|escrev[ae]|produz|desenvolv)\w*/i;
|
|
1004
1098
|
|
|
1099
|
+
/* A outra divisão que compensa: o pedido LONGO que emenda entregas diferentes
|
|
1100
|
+
("cria o site, depois põe login e publica"). Estreita de propósito — o
|
|
1101
|
+
planejador dividindo pedido de uma fase só era o "faz coisas nada a ver" —
|
|
1102
|
+
e o servidor ainda devolve passo único na dúvida. */
|
|
1103
|
+
const RE_ACAO = /\b(cri[ae]r?|fa[zç]a?|fazer|mont[ae]r?|ger[ae]r?|escrev[ae]r?|adicion[ae]r?|coloc[ae]r?|p[oõ]e|implement[ae]r?|public[ae]r?|sob[ea]|instal[ae]r?|configur[ae]r?|test[ae]r?|corrig[ei]r?|arrum[ae]r?|mud[ae]r?|integr[ae]r?|conect[ae]r?)\b/gi;
|
|
1104
|
+
function pedidoDeVariasPartes(pedido) {
|
|
1105
|
+
const t = String(pedido || '');
|
|
1106
|
+
if (t.length < 200) return false;
|
|
1107
|
+
const acoes = new Set((t.match(RE_ACAO) || []).map((x) => x.toLowerCase().slice(0, 4)));
|
|
1108
|
+
return acoes.size >= 3 && /\b(depois|em seguida|e tamb[ée]m|al[ée]m disso|por fim|e ent[ãa]o)\b|\n\s*(\d+[.)]|[-*•])\s/i.test(t);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1005
1111
|
/* "me manda o vídeo" resolvido na hora, sem gastar modelo.
|
|
1006
1112
|
*
|
|
1007
1113
|
* Antes isso abria a última criação do Studio no navegador com `#gravar`, e a
|
|
@@ -1038,12 +1144,33 @@ async function baixarCriacaoDireto(texto) {
|
|
|
1038
1144
|
}
|
|
1039
1145
|
}
|
|
1040
1146
|
|
|
1147
|
+
/* Quantas vezes um passo que parou no meio é retomado sozinho. */
|
|
1148
|
+
const CONTINUACOES = 2;
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* O pedido de retomada: o pedido original + o que já existe, para o agente
|
|
1152
|
+
* seguir em vez de recomeçar. A conversa do passo anterior não vai junto (é
|
|
1153
|
+
* ela que estourou) — vai o que aconteceu no disco, que é o que importa.
|
|
1154
|
+
*/
|
|
1155
|
+
function pedidoDeContinuacao(original, livro) {
|
|
1156
|
+
const linhas = [];
|
|
1157
|
+
if (livro.criados.length) linhas.push(`Arquivos criados: ${livro.criados.slice(0, 20).join(', ')}`);
|
|
1158
|
+
if (livro.alterados.length) linhas.push(`Arquivos alterados: ${livro.alterados.slice(0, 20).join(', ')}`);
|
|
1159
|
+
const cmds = livro.comandos.slice(-8).map((x) => `${x.comando} (${x.ok ? 'ok' : 'falhou'})`);
|
|
1160
|
+
if (cmds.length) linhas.push(`Comandos rodados: ${cmds.join('; ')}`);
|
|
1161
|
+
const falhas = livro.falhas.slice(-3).map((f) => `${f.acao}: ${f.motivo}`);
|
|
1162
|
+
if (falhas.length) linhas.push(`Falhas: ${falhas.join('; ')}`);
|
|
1163
|
+
return `CONTINUAÇÃO — a tarefa abaixo já foi começada e parou no meio. NÃO recomece do zero: `
|
|
1164
|
+
+ `leia o que já existe, veja o que falta, divida o que falta em passos pequenos (write_todos) e termine.\n\n`
|
|
1165
|
+
+ `O QUE JÁ FOI FEITO:\n${linhas.join('\n') || '(nada registrado)'}\n\nA TAREFA:\n${original}`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1041
1168
|
async function dividirTarefa(pedido) {
|
|
1042
1169
|
const inteiro = [{ titulo: 'tarefa', pedido }];
|
|
1043
1170
|
if (!pedido || pedido.trim().length < 12) return inteiro;
|
|
1044
1171
|
// Sem as duas fases no texto, nem chama o servidor: mais rápido, e sem
|
|
1045
1172
|
// dar ao modelo a chance de dividir o que não devia.
|
|
1046
|
-
if (!(RE_DESCOBRIR.test(pedido) && RE_CRIAR_DEPOIS.test(pedido))) return inteiro;
|
|
1173
|
+
if (!(RE_DESCOBRIR.test(pedido) && RE_CRIAR_DEPOIS.test(pedido)) && !pedidoDeVariasPartes(pedido)) return inteiro;
|
|
1047
1174
|
const sp = new Spinner('Primo Code pensando').start();
|
|
1048
1175
|
try {
|
|
1049
1176
|
const r = await api.requestPlano(state.config.server, {
|
|
@@ -1691,6 +1818,27 @@ async function handleSlash(text) {
|
|
|
1691
1818
|
// Mesma memória do terminal: espera o resumo da rodada
|
|
1692
1819
|
// anterior e leva só ele, com o projeto atual.
|
|
1693
1820
|
await esperarResumo();
|
|
1821
|
+
/* O mesmo roteador do terminal: cumprimento e pergunta vão
|
|
1822
|
+
para a conversa, curta; só pedido de trabalho vai para o
|
|
1823
|
+
agente. Antes TUDO ia para o agente — até um "oi". */
|
|
1824
|
+
const escolha = escolherModo(texto, { ultimoModo: state.modoVoz });
|
|
1825
|
+
state.modoVoz = escolha.modo;
|
|
1826
|
+
if (escolha.modo === 'chat') {
|
|
1827
|
+
let resposta = '';
|
|
1828
|
+
try {
|
|
1829
|
+
await api.requestChat(state.config.server, {
|
|
1830
|
+
prompt: `${vozModo.COMO_CONVERSAR}\n\nA PESSOA DISSE: ${texto}`,
|
|
1831
|
+
history: state.history.slice(-4),
|
|
1832
|
+
model: state.config.model,
|
|
1833
|
+
token: state.config.token,
|
|
1834
|
+
}, (evt) => { if (evt.delta) resposta += evt.delta; });
|
|
1835
|
+
} catch { /* sem resposta: a orb volta a ouvir */ }
|
|
1836
|
+
resposta = resposta.trim();
|
|
1837
|
+
state.history.push({ role: 'user', content: texto });
|
|
1838
|
+
state.history.push({ role: 'assistant', content: resposta });
|
|
1839
|
+
resumirDepois();
|
|
1840
|
+
return resposta;
|
|
1841
|
+
}
|
|
1694
1842
|
const plan = prepararPasta(texto);
|
|
1695
1843
|
const resultado = await runAct(texto, {
|
|
1696
1844
|
instrucao: vozModo.COMO_FALAR,
|
|
@@ -1988,6 +2136,11 @@ async function handleSlash(text) {
|
|
|
1988
2136
|
* Eles não somem calados. Quem tem o dedo viciado em `/claude` e
|
|
1989
2137
|
* bate num "comando não existe" conclui que a atualização quebrou
|
|
1990
2138
|
* alguma coisa — e não vai procurar no /help, vai desinstalar. */
|
|
2139
|
+
case 'api': {
|
|
2140
|
+
await comandoApi(rest);
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1991
2144
|
case 'claude': case 'gpt': case 'codex': case 'key': case 'keys': {
|
|
1992
2145
|
console.log(c.muted(' Não existe mais provedor para configurar.'));
|
|
1993
2146
|
console.log(c.muted(' A API já vem com o PrimoCode: entrou na conta, pode usar.'));
|
|
@@ -2045,6 +2198,9 @@ function conferirServidor() {
|
|
|
2045
2198
|
jaConferiu = true;
|
|
2046
2199
|
const minimo = (pkg.primocode && pkg.primocode.servidorMinimo) || '';
|
|
2047
2200
|
if (!minimo) return;
|
|
2201
|
+
// Com a chave própria (/api), quem pensa não é o servidor: a versão dele
|
|
2202
|
+
// não muda nada no que a pessoa pede.
|
|
2203
|
+
if (provedorLocal.atual()) return;
|
|
2048
2204
|
const servidor = state.config.server;
|
|
2049
2205
|
Promise.resolve()
|
|
2050
2206
|
.then(() => api.getHealth(servidor, state.config.token))
|
package/lib/act.js
CHANGED
|
@@ -138,7 +138,10 @@ function logResult(kind, result, profundidade) {
|
|
|
138
138
|
// vermelho que faz parecer que a tarefa morreu.
|
|
139
139
|
if (result.ok === false) {
|
|
140
140
|
const texto = String(result.error || `exit ${result.code ?? 'erro'}`).replace(/\s+/g, ' ').trim();
|
|
141
|
-
|
|
141
|
+
// Erro de permissão não corta: a instrução que resolve ("Acessibilidade…
|
|
142
|
+
// peça de novo") fica justamente depois do 160º caractere.
|
|
143
|
+
const teto = result.permissao === false ? 400 : 160;
|
|
144
|
+
console.log(p + c.warn(texto.length > teto ? texto.slice(0, teto - 1) + '…' : texto));
|
|
142
145
|
return;
|
|
143
146
|
}
|
|
144
147
|
|
|
@@ -411,22 +414,29 @@ function pareceParadaNoMeio(texto, { todosPendentes = 0, rodouTool = false } = {
|
|
|
411
414
|
return false;
|
|
412
415
|
}
|
|
413
416
|
|
|
417
|
+
/* O recado da troca de rota: não repete o que já tentou, quebra o que falta
|
|
418
|
+
em pedaços e faz o primeiro. Quem divide a tarefa é o agente, não a pessoa. */
|
|
419
|
+
const TROCA_DE_ROTA = 'Você ficou três voltas sem conseguir rodar nada, repetindo chamadas que já tinha feito. '
|
|
420
|
+
+ 'Não repita nenhuma delas. Olhe o que já está pronto na conversa, divida o que falta em passos pequenos '
|
|
421
|
+
+ '(write_todos) e faça AGORA o primeiro passo, por um caminho diferente: outra ferramenta, outro arquivo, '
|
|
422
|
+
+ 'outro comando. Se não houver caminho, chame finish e diga em uma frase o que impediu.';
|
|
423
|
+
|
|
414
424
|
function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
415
425
|
if (falhasPendentes.length) {
|
|
416
426
|
return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
|
|
417
427
|
}
|
|
418
428
|
if (travou) {
|
|
419
|
-
return '
|
|
420
|
-
+ '
|
|
429
|
+
return 'Fiquei preso: tentei dois caminhos e os dois voltaram ao mesmo lugar, então parei antes de gastar sua cota à toa. '
|
|
430
|
+
+ 'O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
|
|
421
431
|
}
|
|
422
432
|
if (voltas >= MAX_ITERATIONS) {
|
|
423
433
|
const emFalso = voltasEmFalso
|
|
424
434
|
? ` ${voltasEmFalso} ${voltasEmFalso === 1 ? 'chamada foi repetida' : 'chamadas foram repetidas'} e não rodaram de novo.`
|
|
425
435
|
: '';
|
|
426
|
-
return `
|
|
427
|
-
+ '
|
|
436
|
+
return `Usei as ${MAX_ITERATIONS} voltas deste pedido sem chegar ao fim.${emFalso}`
|
|
437
|
+
+ ' O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
|
|
428
438
|
}
|
|
429
|
-
return 'Parei sem concluir e sem explicação do modelo.
|
|
439
|
+
return 'Parei sem concluir e sem explicação do modelo. Diga "continua" que eu tento de novo daqui.';
|
|
430
440
|
}
|
|
431
441
|
|
|
432
442
|
/* ── O MAPA DO PROJETO ───────────────────────────────────────────────────
|
|
@@ -591,6 +601,7 @@ async function runAct(userPrompt, opts) {
|
|
|
591
601
|
// assim que "crie um vídeo para a tostudy" morreu sem entregar nada.
|
|
592
602
|
let pesquisasSecas = 0;
|
|
593
603
|
let rodadasParadas = 0;
|
|
604
|
+
let trocouDeRota = false;
|
|
594
605
|
/* Para o `pareceParadaNoMeio`: se alguma tool já rodou nesta tarefa, e o
|
|
595
606
|
último plano que o modelo escreveu (write_todos) — desta TAREFA, não da
|
|
596
607
|
sessão: um plano velho, de um pedido interrompido, não pode empurrar o
|
|
@@ -996,6 +1007,7 @@ async function runAct(userPrompt, opts) {
|
|
|
996
1007
|
: await runSubagent(tc.arguments || {}, {
|
|
997
1008
|
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
998
1009
|
});
|
|
1010
|
+
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
999
1011
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
1000
1012
|
continue;
|
|
1001
1013
|
}
|
|
@@ -1027,6 +1039,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1027
1039
|
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, livro, autorizar, controle: portaoControle,
|
|
1028
1040
|
});
|
|
1029
1041
|
logResult(tc.name, r, profundidade);
|
|
1042
|
+
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1030
1043
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
|
|
1031
1044
|
if (r && r.ok && catalogo.escreveArquivo(tc.name)) temPasta = true;
|
|
1032
1045
|
continue;
|
|
@@ -1079,6 +1092,7 @@ async function runAct(userPrompt, opts) {
|
|
|
1079
1092
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
1080
1093
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
1081
1094
|
logResult(tc.name, result, profundidade);
|
|
1095
|
+
repeticao.executou(jaChamado, tc.name, tc.arguments);
|
|
1082
1096
|
if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
|
|
1083
1097
|
// Além de narrar, ANOTA: no fim do pedido sai o balanço do que
|
|
1084
1098
|
// nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
|
|
@@ -1133,6 +1147,16 @@ async function runAct(userPrompt, opts) {
|
|
|
1133
1147
|
// terminar no mesmo lugar — melhor parar e devolver a vez ao usuário.
|
|
1134
1148
|
if (!algoRodou && toolCalls.length) {
|
|
1135
1149
|
rodadasParadas++;
|
|
1150
|
+
/* Antes de desistir, UMA troca de rota, sem aparecer na tela. Parar
|
|
1151
|
+
aqui devolvia ao usuário o trabalho de dividir a tarefa — que é
|
|
1152
|
+
justamente o que ele não sabe fazer e o sistema sabe: o que já
|
|
1153
|
+
foi feito está na conversa, e o que falta é outro caminho. */
|
|
1154
|
+
if (rodadasParadas >= MAX_RODADAS_PARADAS && !trocouDeRota) {
|
|
1155
|
+
trocouDeRota = true;
|
|
1156
|
+
rodadasParadas = 0;
|
|
1157
|
+
messages.push({ role: 'user', content: TROCA_DE_ROTA });
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1136
1160
|
// Quem fala é o motivoDaParada, uma vez só — aqui havia uma
|
|
1137
1161
|
// segunda frase de "travei", e as duas saíam em sequência.
|
|
1138
1162
|
if (rodadasParadas >= MAX_RODADAS_PARADAS) break;
|
package/lib/api.js
CHANGED
|
@@ -201,14 +201,28 @@ function requestStream(serverUrl, endpoint, payload, onEvent, token, sinal) {
|
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/* Com um provedor configurado (/api), estas quatro rotas rodam AQUI,
|
|
205
|
+
com a chave da pessoa, em vez de ir ao servidor — ver lib/local/motor.js.
|
|
206
|
+
O resto (login, transcrição, conectores) continua no servidor. */
|
|
207
|
+
function local() {
|
|
208
|
+
try {
|
|
209
|
+
const motor = require('./local/motor.js');
|
|
210
|
+
return motor.ativo() ? motor : null;
|
|
211
|
+
} catch { return null; }
|
|
212
|
+
}
|
|
213
|
+
|
|
204
214
|
// Agente autônomo. `payload` carrega a conversa completa (messages).
|
|
205
215
|
function requestAct(serverUrl, payload, onEvent) {
|
|
216
|
+
const m = local();
|
|
217
|
+
if (m) return m.requestAct(serverUrl, payload, onEvent);
|
|
206
218
|
const { token, sinal, ...body } = payload;
|
|
207
219
|
return requestStream(serverUrl, '/api/act/stream', body, onEvent, token, sinal);
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
// Chat puro.
|
|
211
223
|
function requestChat(serverUrl, payload, onEvent) {
|
|
224
|
+
const m = local();
|
|
225
|
+
if (m) return m.requestChat(serverUrl, payload, onEvent);
|
|
212
226
|
const { token, sinal, ...body } = payload;
|
|
213
227
|
return requestStream(serverUrl, '/api/chat/stream', body, onEvent, token, sinal);
|
|
214
228
|
}
|
|
@@ -216,6 +230,8 @@ function requestChat(serverUrl, payload, onEvent) {
|
|
|
216
230
|
// Resumo incremental da conversa. Vai e volta pequeno: manda o resumo de
|
|
217
231
|
// antes + o que é novo, recebe o resumo atualizado.
|
|
218
232
|
function requestResumo(serverUrl, payload) {
|
|
233
|
+
const m = local();
|
|
234
|
+
if (m) return m.requestResumo(serverUrl, payload);
|
|
219
235
|
const { token, ...body } = payload;
|
|
220
236
|
return request(serverUrl, '/api/resumir', body, token);
|
|
221
237
|
}
|
|
@@ -236,6 +252,8 @@ function requestRefino(serverUrl, payload) {
|
|
|
236
252
|
|
|
237
253
|
// O plano: a tarefa grande dividida em passos, para uma chamada por pedaço.
|
|
238
254
|
function requestPlano(serverUrl, payload) {
|
|
255
|
+
const m = local();
|
|
256
|
+
if (m) return m.requestPlano(serverUrl, payload);
|
|
239
257
|
const { token, ...body } = payload;
|
|
240
258
|
return request(serverUrl, '/api/plano', body, token);
|
|
241
259
|
}
|
package/lib/browser.js
CHANGED
|
@@ -463,6 +463,9 @@ async function ensure(url) {
|
|
|
463
463
|
// ---- Ações ------------------------------------------------------
|
|
464
464
|
async function open(url) {
|
|
465
465
|
await ensure(url);
|
|
466
|
+
// A janela vem para a frente: quem pediu "abre o youtube" quer VER abrir,
|
|
467
|
+
// não descobrir depois que havia um Chrome atrás do terminal.
|
|
468
|
+
if (verSeta()) { try { await currentSession.send('Page.bringToFront'); } catch { /* segue */ } }
|
|
466
469
|
// Navega até URL solicitada
|
|
467
470
|
await currentSession.send('Page.navigate', { url });
|
|
468
471
|
// Espera "load" simples via polling do document.readyState
|
|
@@ -486,8 +489,73 @@ async function evaluate(js) {
|
|
|
486
489
|
return r.result ? r.result.value : undefined;
|
|
487
490
|
}
|
|
488
491
|
|
|
489
|
-
|
|
492
|
+
/* ── A SETA DO PRIMO DENTRO DA PÁGINA ────────────────────────────────────
|
|
493
|
+
* "Quando a IA vai mexer no computador, eu não consigo ver ela mexendo."
|
|
494
|
+
*
|
|
495
|
+
* O clique no navegador vai pelo CDP (Input.dispatchMouseEvent): acontece de
|
|
496
|
+
* verdade, mas nada anda na tela — a página só muda, do nada. Medido no
|
|
497
|
+
* pedido "abra o youtube e abra o vídeo mais recente". Então, antes de cada
|
|
498
|
+
* clique e de cada digitação, uma seta desenhada NA página desliza até o
|
|
499
|
+
* alvo com o que ela vai fazer ("clicando em Pesquisar"), e o clique deixa
|
|
500
|
+
* uma onda. É DOM, não o mouse do sistema: funciona em qualquer SO, sem
|
|
501
|
+
* permissão, e não rouba o mouse de quem está usando o computador.
|
|
502
|
+
*
|
|
503
|
+
* PRIMOCODE_SEM_CURSOR=1 (e o navegador sem tela) desliga: nos testes e no
|
|
504
|
+
* servidor ninguém está olhando, e a animação só custaria tempo. */
|
|
505
|
+
const DESLIZE_MS = 450;
|
|
506
|
+
function verSeta() {
|
|
507
|
+
return process.env.PRIMOCODE_SEM_CURSOR !== '1'
|
|
508
|
+
&& !(process.env.PRIMOCODE_BROWSER_HEADLESS === '1' || process.env.PRIMOCODE_BROWSER_HEADLESS === 'true');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function scriptDaSeta(x, y, rotulo, onda) {
|
|
512
|
+
return `(function(){
|
|
513
|
+
var id = '__primo_seta';
|
|
514
|
+
var s = document.getElementById(id);
|
|
515
|
+
if (!s) {
|
|
516
|
+
s = document.createElement('div');
|
|
517
|
+
s.id = id;
|
|
518
|
+
s.setAttribute('aria-hidden', 'true');
|
|
519
|
+
s.style.cssText = 'position:fixed;left:0;top:0;z-index:2147483647;pointer-events:none;'
|
|
520
|
+
+ 'transition:transform ${DESLIZE_MS}ms cubic-bezier(.2,.7,.2,1);will-change:transform;'
|
|
521
|
+
+ 'transform:translate(' + (innerWidth / 2) + 'px,' + (innerHeight / 2) + 'px)';
|
|
522
|
+
s.innerHTML = '<svg width="26" height="26" viewBox="0 0 24 24" style="display:block;filter:drop-shadow(0 2px 3px rgba(2,6,23,.45))">'
|
|
523
|
+
+ '<path d="M3 2 L3 19 L8 14.5 L11.5 22 L14.5 20.6 L11 13.2 L18 13.2 Z" fill="#22d3ee" stroke="#0f172a" stroke-width="1.4" stroke-linejoin="round"/></svg>'
|
|
524
|
+
+ '<div data-rotulo style="position:absolute;left:24px;top:18px;white-space:nowrap;font:600 12px/1 system-ui,sans-serif;'
|
|
525
|
+
+ 'color:#0f172a;background:#22d3ee;padding:5px 8px;border-radius:6px;box-shadow:0 2px 6px rgba(2,6,23,.3)"></div>';
|
|
526
|
+
document.documentElement.appendChild(s);
|
|
527
|
+
s.getBoundingClientRect();
|
|
528
|
+
}
|
|
529
|
+
var r = s.querySelector('[data-rotulo]');
|
|
530
|
+
r.textContent = ${JSON.stringify(String(rotulo || '').slice(0, 48))};
|
|
531
|
+
r.style.display = r.textContent ? 'block' : 'none';
|
|
532
|
+
s.style.transform = 'translate(${Math.round(x)}px,${Math.round(y)}px)';
|
|
533
|
+
if (${onda ? 'true' : 'false'}) setTimeout(function(){
|
|
534
|
+
var o = document.createElement('div');
|
|
535
|
+
o.style.cssText = 'position:fixed;left:${Math.round(x) - 16}px;top:${Math.round(y) - 16}px;width:32px;height:32px;'
|
|
536
|
+
+ 'border-radius:50%;border:3px solid #22d3ee;z-index:2147483646;pointer-events:none;'
|
|
537
|
+
+ 'transition:transform .5s ease-out,opacity .5s ease-out;transform:scale(.3);opacity:1';
|
|
538
|
+
document.documentElement.appendChild(o);
|
|
539
|
+
o.getBoundingClientRect();
|
|
540
|
+
o.style.transform = 'scale(1.6)'; o.style.opacity = '0';
|
|
541
|
+
setTimeout(function(){ o.remove(); }, 600);
|
|
542
|
+
}, ${DESLIZE_MS});
|
|
543
|
+
return true;
|
|
544
|
+
})()`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Leva a seta até (x, y) e espera ela chegar. Nunca falha a ação por causa dela. */
|
|
548
|
+
async function mostrarSeta(x, y, rotulo = '', { onda = true } = {}) {
|
|
549
|
+
if (!verSeta() || !currentSession) return;
|
|
550
|
+
try {
|
|
551
|
+
await currentSession.send('Runtime.evaluate', { expression: scriptDaSeta(x, y, rotulo, onda) });
|
|
552
|
+
await sleep(DESLIZE_MS + 60);
|
|
553
|
+
} catch { /* a seta é para quem olha; a ação segue sem ela */ }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function click(x, y, rotulo = 'clicando') {
|
|
490
557
|
await ensure();
|
|
558
|
+
await mostrarSeta(x, y, rotulo);
|
|
491
559
|
await currentSession.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
|
492
560
|
await currentSession.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
493
561
|
await currentSession.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
@@ -507,7 +575,8 @@ async function clickText(text) {
|
|
|
507
575
|
if (r.width && r.height) {
|
|
508
576
|
el.scrollIntoView({block:'center'});
|
|
509
577
|
const r2 = el.getBoundingClientRect();
|
|
510
|
-
|
|
578
|
+
const nome = (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().replace(/\s+/g, ' ');
|
|
579
|
+
return { x: r2.left + r2.width/2, y: r2.top + r2.height/2, tag: el.tagName, text: s.slice(0,80), nome: nome.slice(0,30) };
|
|
511
580
|
}
|
|
512
581
|
}
|
|
513
582
|
}
|
|
@@ -516,7 +585,7 @@ async function clickText(text) {
|
|
|
516
585
|
const found = await evaluate(js);
|
|
517
586
|
if (!found) throw new Error(`nenhum elemento clicável encontrado com texto: ${text}`);
|
|
518
587
|
await sleep(200);
|
|
519
|
-
await click(found.x, found.y);
|
|
588
|
+
await click(found.x, found.y, `clicando em ${found.nome || found.text}`.slice(0, 44));
|
|
520
589
|
return { ok: true, ...found };
|
|
521
590
|
}
|
|
522
591
|
|
|
@@ -595,13 +664,14 @@ async function clickSelector(selector) {
|
|
|
595
664
|
if (!el) return null;
|
|
596
665
|
el.scrollIntoView({block:'center'});
|
|
597
666
|
const r = el.getBoundingClientRect();
|
|
598
|
-
|
|
667
|
+
const nome = (el.getAttribute('aria-label') || el.innerText || el.value || el.placeholder || '').trim().replace(/\s+/g, ' ').slice(0, 30);
|
|
668
|
+
return { x: r.left + r.width/2, y: r.top + r.height/2, tag: el.tagName, nome };
|
|
599
669
|
})()`;
|
|
600
670
|
let found = await evaluate(js);
|
|
601
671
|
if (!found && await esperarSeletor(selector)) found = await evaluate(js);
|
|
602
672
|
if (!found) throw await erroDeSeletor(selector);
|
|
603
673
|
await sleep(200);
|
|
604
|
-
await click(found.x, found.y);
|
|
674
|
+
await click(found.x, found.y, found.nome ? `clicando em ${found.nome}`.slice(0, 44) : 'clicando');
|
|
605
675
|
return { ok: true, selector, ...found };
|
|
606
676
|
}
|
|
607
677
|
|
|
@@ -619,13 +689,16 @@ async function typeInSelector(selector, text, clear = true) {
|
|
|
619
689
|
const focar = () => evaluate(`(function(){
|
|
620
690
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
621
691
|
if (!el) return false;
|
|
692
|
+
el.scrollIntoView({block:'center'});
|
|
622
693
|
el.focus();
|
|
623
694
|
${clear ? "if ('value' in el) el.value = '';" : ''}
|
|
624
|
-
|
|
695
|
+
const r = el.getBoundingClientRect();
|
|
696
|
+
return { x: r.left + Math.min(24, r.width / 2), y: r.top + r.height / 2 };
|
|
625
697
|
})()`);
|
|
626
698
|
let focused = await focar();
|
|
627
699
|
if (!focused && await esperarSeletor(selector)) focused = await focar();
|
|
628
700
|
if (!focused) throw await erroDeSeletor(selector);
|
|
701
|
+
await mostrarSeta(focused.x, focused.y, `digitando: ${text}`.slice(0, 44), { onda: false });
|
|
629
702
|
await type(text);
|
|
630
703
|
// dispara evento input para frameworks reativos
|
|
631
704
|
await evaluate(`(function(){
|
|
@@ -1043,6 +1116,7 @@ function fecharRevisor() {
|
|
|
1043
1116
|
}
|
|
1044
1117
|
|
|
1045
1118
|
module.exports = {
|
|
1119
|
+
scriptDaSeta, verSeta,
|
|
1046
1120
|
open, click, clickText, clickSelector, type, typeInSelector,
|
|
1047
1121
|
key, screenshot, evaluate, waitFor, wait, scroll, close,
|
|
1048
1122
|
inspecionarEscondido, rodarEscondido, abrirEscondido, fecharRevisor,
|