primocode 8.2.1 → 8.4.0
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/bin/primocode.js +42 -3
- package/lib/api.js +8 -1
- package/lib/memoria-conversa.js +124 -0
- package/lib/referencia.js +87 -7
- package/lib/studio.js +33 -4
- package/package.json +2 -2
package/bin/primocode.js
CHANGED
|
@@ -24,6 +24,7 @@ const claudeEngine = require('../lib/claude-engine');
|
|
|
24
24
|
const pasta = require('../lib/pasta');
|
|
25
25
|
const catalogo = require('../lib/catalogo');
|
|
26
26
|
const effort = require('../lib/effort');
|
|
27
|
+
const memoria = require('../lib/memoria-conversa');
|
|
27
28
|
|
|
28
29
|
// Rede de segurança de último recurso: sem isto, uma exceção fora de um
|
|
29
30
|
// try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
|
|
@@ -50,6 +51,9 @@ const state = {
|
|
|
50
51
|
mode: null, // 'primocode' | 'chat' | 'auto' — vem da config em main()
|
|
51
52
|
modoEfetivo: null, // no auto, o modo que o último pedido usou de fato
|
|
52
53
|
attachedFiles: [],
|
|
54
|
+
// O resumo incremental da conversa — um /compact automático. O porquê
|
|
55
|
+
// está em lib/memoria-conversa.js.
|
|
56
|
+
memoria: memoria.nova(),
|
|
53
57
|
lastResponse: '',
|
|
54
58
|
currentProject: null,
|
|
55
59
|
workDir: null, // pasta onde o agente trabalha nesta sessão
|
|
@@ -529,12 +533,17 @@ async function sendPrompt(userText) {
|
|
|
529
533
|
if (modo === 'primocode') {
|
|
530
534
|
const plan = prepararPasta(userText);
|
|
531
535
|
|
|
536
|
+
// Antes de gastar com a tarefa, dobra no resumo o que já passou. A
|
|
537
|
+
// conta é pequena: entra "resumo de antes + o que é novo", não a
|
|
538
|
+
// conversa inteira.
|
|
539
|
+
await comprimirConversa();
|
|
540
|
+
|
|
532
541
|
const r = await runAct(prompt, {
|
|
533
542
|
server: state.config.server,
|
|
534
543
|
model: state.config.model,
|
|
535
544
|
effort: state.config.effort,
|
|
536
545
|
token: state.config.token,
|
|
537
|
-
history: state.history,
|
|
546
|
+
history: memoria.paraEnviar(state.memoria, state.history),
|
|
538
547
|
projectDir: plan.dir,
|
|
539
548
|
projectName: plan.name || state.currentProject,
|
|
540
549
|
// Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
|
|
@@ -547,6 +556,7 @@ async function sendPrompt(userText) {
|
|
|
547
556
|
state.lastResponse = r.assistantMessage.content;
|
|
548
557
|
cfg.appendHistory({ mode: 'primocode', project: state.currentProject, prompt: userText });
|
|
549
558
|
} else {
|
|
559
|
+
await comprimirConversa();
|
|
550
560
|
await runChat(prompt, userText);
|
|
551
561
|
}
|
|
552
562
|
|
|
@@ -554,6 +564,27 @@ async function sendPrompt(userText) {
|
|
|
554
564
|
refreshPrompt();
|
|
555
565
|
}
|
|
556
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Atualiza o resumo da conversa, se já compensar.
|
|
569
|
+
*
|
|
570
|
+
* Silencioso quando não há o que fazer: nas primeiras rodadas a conversa é
|
|
571
|
+
* curta e o resumo não pagaria a chamada.
|
|
572
|
+
*/
|
|
573
|
+
async function comprimirConversa() {
|
|
574
|
+
if (!memoria.precisaResumir(state.memoria, state.history)) return;
|
|
575
|
+
const sp = new Spinner('resumindo a conversa').start();
|
|
576
|
+
try {
|
|
577
|
+
state.memoria = await memoria.atualizar(state.memoria, state.history, ({ resumo, mensagens }) =>
|
|
578
|
+
api.requestResumo(state.config.server, { resumo, mensagens, token: state.config.token })
|
|
579
|
+
.then((r) => r.resumo));
|
|
580
|
+
sp.stop();
|
|
581
|
+
const e = memoria.economia(state.memoria, state.history);
|
|
582
|
+
if (e.poupado > 0) console.log(c.dim(` ⎿ conversa resumida · ${e.cheio} → ${e.enxuto} tokens por pedido`));
|
|
583
|
+
} catch {
|
|
584
|
+
sp.stop(); // resumo é economia, não requisito: falhou, segue a vida
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
557
588
|
async function runChat(promptForModel, userTextForMemory) {
|
|
558
589
|
const sp = new Spinner('pensando').start();
|
|
559
590
|
let started = false;
|
|
@@ -561,7 +592,7 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
561
592
|
try {
|
|
562
593
|
await api.requestChat(state.config.server, {
|
|
563
594
|
prompt: promptForModel,
|
|
564
|
-
history: state.history
|
|
595
|
+
history: memoria.paraEnviar(state.memoria, state.history),
|
|
565
596
|
model: state.config.model,
|
|
566
597
|
token: state.config.token,
|
|
567
598
|
}, (evt) => {
|
|
@@ -854,7 +885,7 @@ async function handleSlash(text) {
|
|
|
854
885
|
break;
|
|
855
886
|
}
|
|
856
887
|
|
|
857
|
-
case 'clear': state.history = []; console.log(c.ok('Conversa limpa.')); break;
|
|
888
|
+
case 'clear': state.history = []; state.memoria = memoria.nova(); console.log(c.ok('Conversa limpa.')); break;
|
|
858
889
|
case 'cls': process.stdout.write('\x1Bc'); break;
|
|
859
890
|
|
|
860
891
|
case 'status': {
|
|
@@ -872,6 +903,14 @@ async function handleSlash(text) {
|
|
|
872
903
|
console.log(c.dim(` Reservas: ${fila.length - 1}`));
|
|
873
904
|
}
|
|
874
905
|
} catch (e) { sp.stop(); console.log(c.err(`Indisponível: ${e.message}`)); }
|
|
906
|
+
// A memória da conversa: quanto ela está poupando por pedido.
|
|
907
|
+
if (state.history.length) {
|
|
908
|
+
const e = memoria.economia(state.memoria, state.history);
|
|
909
|
+
console.log(c.muted(` Conversa: ${state.history.length} mensagens`)
|
|
910
|
+
+ (state.memoria.resumo
|
|
911
|
+
? c.dim(` · resumida, ${e.enxuto} tokens por pedido em vez de ${e.cheio}`)
|
|
912
|
+
: c.dim(` · ${e.enxuto} tokens por pedido`)));
|
|
913
|
+
}
|
|
875
914
|
break;
|
|
876
915
|
}
|
|
877
916
|
|
package/lib/api.js
CHANGED
|
@@ -128,6 +128,13 @@ function requestChat(serverUrl, payload, onEvent) {
|
|
|
128
128
|
return requestStream(serverUrl, '/api/chat/stream', body, onEvent, token);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Resumo incremental da conversa. Vai e volta pequeno: manda o resumo de
|
|
132
|
+
// antes + o que é novo, recebe o resumo atualizado.
|
|
133
|
+
function requestResumo(serverUrl, payload) {
|
|
134
|
+
const { token, ...body } = payload;
|
|
135
|
+
return request(serverUrl, '/api/resumir', body, token);
|
|
136
|
+
}
|
|
137
|
+
|
|
131
138
|
function getHealth(serverUrl, token) {
|
|
132
139
|
return new Promise((resolve, reject) => {
|
|
133
140
|
const url = new URL('/health', serverUrl);
|
|
@@ -144,4 +151,4 @@ function getHealth(serverUrl, token) {
|
|
|
144
151
|
});
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
module.exports = { request, requestStream, requestAct, requestChat, getHealth };
|
|
154
|
+
module.exports = { request, requestStream, requestAct, requestChat, requestResumo, getHealth };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memoria-conversa.js — a conversa que não engorda.
|
|
3
|
+
*
|
|
4
|
+
* ── O PROBLEMA ───────────────────────────────────────────────────────────
|
|
5
|
+
* O histórico ia inteiro (as últimas 20 mensagens) em todo pedido. Numa
|
|
6
|
+
* conversa de trabalho isso vira milhares de tokens repetidos a cada rodada, e
|
|
7
|
+
* num plano com teto por minuto a tarefa morre no meio — depois de o esforço
|
|
8
|
+
* já ter sido gasto. Cortar pelas últimas N é pior ainda: some justamente o
|
|
9
|
+
* começo, onde está o que o usuário pediu.
|
|
10
|
+
*
|
|
11
|
+
* ── A IDEIA (do dono) ────────────────────────────────────────────────────
|
|
12
|
+
* Resumo INCREMENTAL, como um /compact automático: guarda-se um resumo do que
|
|
13
|
+
* já passou e, a cada rodada, só o PEDAÇO NOVO é dobrado nele. A primeira vez
|
|
14
|
+
* custa; da segunda em diante o resumo já existe e só o que foi dito a mais
|
|
15
|
+
* precisa ser lido.
|
|
16
|
+
*
|
|
17
|
+
* O custo fica limitado por construção: a entrada do resumo é sempre
|
|
18
|
+
* "resumo de antes + o que aconteceu desde então", nunca a conversa inteira.
|
|
19
|
+
*
|
|
20
|
+
* ── O QUE NUNCA É RESUMIDO ───────────────────────────────────────────────
|
|
21
|
+
* As últimas rodadas vão CRUAS. Resumo é bom para lembrar do que foi feito,
|
|
22
|
+
* péssimo para continuar de onde parou: "agora põe música de fundo" precisa da
|
|
23
|
+
* frase anterior literal, não de um tópico dizendo que houve um vídeo.
|
|
24
|
+
*
|
|
25
|
+
* Este arquivo é puro — quem fala com o servidor é quem chama `atualizar`.
|
|
26
|
+
* Assim o teste mede a política inteira sem rede e sem chave de API.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
/** ~4 caracteres por token. Mesma conta do servidor, e boa o bastante aqui. */
|
|
32
|
+
function tokensDe(x) {
|
|
33
|
+
const texto = typeof x === 'string' ? x : JSON.stringify(x || '');
|
|
34
|
+
return Math.ceil(texto.length / 4);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Quantas mensagens do fim ficam sempre cruas (o "de onde parei").
|
|
38
|
+
const CRUAS = 4;
|
|
39
|
+
// Acima disto, a cauda vira resumo. Abaixo, não compensa pagar a chamada.
|
|
40
|
+
const LIMITE_TOKENS = 700;
|
|
41
|
+
|
|
42
|
+
/** Estado inicial — o que o CLI guarda entre um pedido e outro. */
|
|
43
|
+
function nova() {
|
|
44
|
+
return { resumo: '', resumidasAte: 0 };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* O que ainda não foi resumido e já não cabe entre as cruas.
|
|
49
|
+
* É exatamente o que a próxima chamada de resumo precisa ler.
|
|
50
|
+
*/
|
|
51
|
+
function pendentes(memoria, history) {
|
|
52
|
+
const fim = Math.max(0, (history || []).length - CRUAS);
|
|
53
|
+
if (fim <= memoria.resumidasAte) return [];
|
|
54
|
+
return history.slice(memoria.resumidasAte, fim);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Vale a pena pagar uma chamada de resumo agora? */
|
|
58
|
+
function precisaResumir(memoria, history) {
|
|
59
|
+
const novas = pendentes(memoria, history);
|
|
60
|
+
if (!novas.length) return false;
|
|
61
|
+
return tokensDe(novas) >= LIMITE_TOKENS;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dobra o pedaço novo no resumo. `resumir` é quem fala com o servidor:
|
|
66
|
+
* recebe { resumo, mensagens } e devolve o texto do resumo atualizado.
|
|
67
|
+
*
|
|
68
|
+
* Devolve SEMPRE uma memória utilizável — se o resumo falhar (rede, cota), a
|
|
69
|
+
* anterior continua valendo e a conversa segue com o histórico cru.
|
|
70
|
+
*/
|
|
71
|
+
async function atualizar(memoria, history, resumir) {
|
|
72
|
+
const novas = pendentes(memoria, history);
|
|
73
|
+
if (!novas.length) return memoria;
|
|
74
|
+
|
|
75
|
+
let texto;
|
|
76
|
+
try {
|
|
77
|
+
texto = await resumir({ resumo: memoria.resumo, mensagens: novas });
|
|
78
|
+
} catch {
|
|
79
|
+
return memoria;
|
|
80
|
+
}
|
|
81
|
+
const limpo = String(texto || '').trim();
|
|
82
|
+
if (!limpo) return memoria;
|
|
83
|
+
|
|
84
|
+
return { resumo: limpo, resumidasAte: (history || []).length - CRUAS };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Onde a parte crua pode começar sem deixar resultado órfão.
|
|
89
|
+
*
|
|
90
|
+
* A API recusa um `role:'tool'` sem o turno do assistente que o pediu. Cortar
|
|
91
|
+
* "as últimas N" cai nisso na hora em que a conta não bate — então o começo
|
|
92
|
+
* recua até um ponto onde a rodada está inteira.
|
|
93
|
+
*/
|
|
94
|
+
function inicioSeguro(todas, alvo) {
|
|
95
|
+
let i = Math.max(0, Math.min(alvo, todas.length));
|
|
96
|
+
while (i > 0 && todas[i] && todas[i].role === 'tool') i--;
|
|
97
|
+
return i;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* O histórico que vai no pedido: o resumo (se houver) + as últimas cruas.
|
|
102
|
+
*
|
|
103
|
+
* O resumo entra como mensagem de sistema, não como fala do usuário — o modelo
|
|
104
|
+
* precisa saber que aquilo é memória, e não algo que alguém acabou de dizer.
|
|
105
|
+
*/
|
|
106
|
+
function paraEnviar(memoria, history) {
|
|
107
|
+
const todas = history || [];
|
|
108
|
+
if (!memoria || !memoria.resumo) return todas.slice(inicioSeguro(todas, todas.length - 20));
|
|
109
|
+
|
|
110
|
+
const cruas = todas.slice(inicioSeguro(todas, todas.length - CRUAS));
|
|
111
|
+
return [
|
|
112
|
+
{ role: 'system', content: `Resumo da conversa até aqui (memória, não é fala de ninguém):\n${memoria.resumo}` },
|
|
113
|
+
...cruas,
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Quanto este jeito economiza — para o /status e para o teste provarem. */
|
|
118
|
+
function economia(memoria, history) {
|
|
119
|
+
const cheio = tokensDe((history || []).slice(-20));
|
|
120
|
+
const enxuto = tokensDe(paraEnviar(memoria, history));
|
|
121
|
+
return { cheio, enxuto, poupado: Math.max(0, cheio - enxuto) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { nova, pendentes, precisaResumir, atualizar, paraEnviar, economia, tokensDe, inicioSeguro, CRUAS, LIMITE_TOKENS };
|
package/lib/referencia.js
CHANGED
|
@@ -102,10 +102,17 @@ function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
|
|
|
102
102
|
if (hostBloqueado(alvo.hostname)) return resolve(null);
|
|
103
103
|
const cliente = alvo.protocol === 'http:' ? http : https;
|
|
104
104
|
|
|
105
|
+
// O User-Agent precisa parecer navegador. Buscador nenhum responde a
|
|
106
|
+
// agente desconhecido: DuckDuckGo, Bing e Searx devolviam página VAZIA
|
|
107
|
+
// para o 'PrimoCode/1.0' — e o usuário via "nada encontrado (sem
|
|
108
|
+
// internet?)" com a internet funcionando perfeitamente. Site comum não
|
|
109
|
+
// liga para isso, e por isso as páginas abriam: só a BUSCA morria.
|
|
105
110
|
const opcoes = {
|
|
106
111
|
headers: {
|
|
107
|
-
'User-Agent': '
|
|
108
|
-
|
|
112
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
|
|
113
|
+
+ '(KHTML, like Gecko) Chrome/122.0 Safari/537.36',
|
|
114
|
+
Accept: 'text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8',
|
|
115
|
+
'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',
|
|
109
116
|
},
|
|
110
117
|
timeout,
|
|
111
118
|
};
|
|
@@ -191,6 +198,59 @@ async function buscarDDG(consulta, limite) {
|
|
|
191
198
|
return limparLinks(pares, limite);
|
|
192
199
|
}
|
|
193
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Wikipedia — a fonte que NÃO bloqueia.
|
|
203
|
+
*
|
|
204
|
+
* Buscador raspado por HTML (DDG, Bing, Searx) recusa quem não parece gente:
|
|
205
|
+
* devolve 202/302 e página vazia, e o usuário lê "nada encontrado (sem
|
|
206
|
+
* internet?)" com a internet ótima. Medido: no IP deste servidor os três
|
|
207
|
+
* calaram juntos, enquanto a API da Wikipedia respondeu 200 na hora.
|
|
208
|
+
*
|
|
209
|
+
* É API pública e estável, feita para ser consumida por programa — e para
|
|
210
|
+
* empresa, pessoa, cidade e evento costuma ser a MELHOR fonte, não a pior.
|
|
211
|
+
*/
|
|
212
|
+
async function buscarWikipedia(consulta, limite) {
|
|
213
|
+
const achados = [];
|
|
214
|
+
for (const lingua of ['pt', 'en']) {
|
|
215
|
+
if (achados.length >= limite) break;
|
|
216
|
+
const url = `https://${lingua}.wikipedia.org/w/api.php?action=query&list=search`
|
|
217
|
+
+ `&srsearch=${encodeURIComponent(consulta)}&format=json&srlimit=${limite}`;
|
|
218
|
+
const bruto = await pegar(url);
|
|
219
|
+
if (!bruto) continue;
|
|
220
|
+
let j;
|
|
221
|
+
try { j = JSON.parse(bruto); } catch { continue; }
|
|
222
|
+
for (const r of (j.query && j.query.search) || []) {
|
|
223
|
+
achados.push({
|
|
224
|
+
titulo: r.title,
|
|
225
|
+
url: `https://${lingua}.wikipedia.org/wiki/${encodeURIComponent(r.title.replace(/ /g, '_'))}`,
|
|
226
|
+
nota: 'Wikipédia',
|
|
227
|
+
});
|
|
228
|
+
if (achados.length >= limite) break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return achados;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A resposta direta do DuckDuckGo (API oficial, sem raspagem).
|
|
236
|
+
*
|
|
237
|
+
* Não é busca de links: é o resumo que ele já tem sobre o assunto, mais os
|
|
238
|
+
* tópicos relacionados. Responde 200 onde a página de busca devolve 202.
|
|
239
|
+
*/
|
|
240
|
+
async function buscarRespostaDDG(consulta, limite) {
|
|
241
|
+
const bruto = await pegar(`https://api.duckduckgo.com/?q=${encodeURIComponent(consulta)}&format=json&no_html=1&skip_disambig=1`);
|
|
242
|
+
if (!bruto) return [];
|
|
243
|
+
let j;
|
|
244
|
+
try { j = JSON.parse(bruto); } catch { return []; }
|
|
245
|
+
const achados = [];
|
|
246
|
+
if (j.AbstractURL) achados.push({ titulo: j.Heading || consulta, url: j.AbstractURL, nota: j.AbstractSource });
|
|
247
|
+
for (const t of j.RelatedTopics || []) {
|
|
248
|
+
if (achados.length >= limite) break;
|
|
249
|
+
if (t.FirstURL) achados.push({ titulo: (t.Text || '').slice(0, 120), url: t.FirstURL });
|
|
250
|
+
}
|
|
251
|
+
return limparLinks(achados, limite);
|
|
252
|
+
}
|
|
253
|
+
|
|
194
254
|
/** Bing (HTML) — a reserva quando o DDG não responde. */
|
|
195
255
|
async function buscarBing(consulta, limite) {
|
|
196
256
|
const html = await pegar(`https://www.bing.com/search?q=${encodeURIComponent(consulta)}&setlang=en`);
|
|
@@ -218,13 +278,24 @@ async function buscarSearx(consulta, limite) {
|
|
|
218
278
|
* pesquisa inteira. Sem chave de API em nenhuma delas.
|
|
219
279
|
*/
|
|
220
280
|
async function buscar(consulta, limite = MAX_POR_FONTE) {
|
|
221
|
-
|
|
281
|
+
// Ordem deliberada: primeiro os buscadores raspados (dão os links mais
|
|
282
|
+
// frescos, quando respondem), depois as APIs que NUNCA bloqueiam. Antes a
|
|
283
|
+
// fila acabava no Searx e, com os três calados, a pesquisa inteira voltava
|
|
284
|
+
// vazia — mesmo havendo resposta boa na Wikipédia.
|
|
285
|
+
const portas = [buscarDDG, buscarBing, buscarSearx, buscarRespostaDDG, buscarWikipedia];
|
|
286
|
+
const juntos = [];
|
|
287
|
+
for (const porta of portas) {
|
|
222
288
|
try {
|
|
223
289
|
const achados = await porta(consulta, limite);
|
|
224
|
-
|
|
290
|
+
for (const a of achados) {
|
|
291
|
+
if (!juntos.some((j) => j.url === a.url)) juntos.push(a);
|
|
292
|
+
}
|
|
293
|
+
// Os raspados, quando respondem, já bastam. As APIs entram para
|
|
294
|
+
// completar quando eles vieram vazios.
|
|
295
|
+
if (juntos.length >= limite) break;
|
|
225
296
|
} catch {}
|
|
226
297
|
}
|
|
227
|
-
return
|
|
298
|
+
return juntos.slice(0, limite);
|
|
228
299
|
}
|
|
229
300
|
|
|
230
301
|
/** Respostas do StackOverflow — a API pública, sem chave. */
|
|
@@ -284,7 +355,16 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
284
355
|
const candidatos = [...web, ...so, ...docsConhecidas(termo)]
|
|
285
356
|
.slice(0, Math.max(2, Math.min(8, paginas + 4)));
|
|
286
357
|
if (!candidatos.length) {
|
|
287
|
-
return {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
error: `Nenhuma fonte respondeu sobre "${termo}".`,
|
|
361
|
+
consulta: termo,
|
|
362
|
+
// "sem internet?" era o palpite errado, e mandava o usuário conferir
|
|
363
|
+
// a conexão dele: medido, os buscadores devolvem 202/302 e página
|
|
364
|
+
// vazia para quem não parece navegador, com a rede perfeita.
|
|
365
|
+
provavelCausa: 'Os buscadores costumam recusar chamadas automáticas (bloqueio ou limite por excesso de buscas seguidas), e não a sua conexão.',
|
|
366
|
+
comoCorrigir: 'Tente OUTRA consulta, mais curta e específica. Se souber o site do assunto, vá direto nele com browser_open + browser_read — é o caminho mais confiável para fato de empresa.',
|
|
367
|
+
};
|
|
288
368
|
}
|
|
289
369
|
|
|
290
370
|
const lidas = [];
|
|
@@ -316,4 +396,4 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
316
396
|
};
|
|
317
397
|
}
|
|
318
398
|
|
|
319
|
-
module.exports = { pesquisar, buscar, stackoverflow, textoDe };
|
|
399
|
+
module.exports = { pesquisar, buscar, stackoverflow, textoDe, buscarWikipedia, buscarRespostaDDG };
|
package/lib/studio.js
CHANGED
|
@@ -41,8 +41,31 @@ const BASE = `http://127.0.0.1:${PORTA}`;
|
|
|
41
41
|
// criou) nunca são tocados.
|
|
42
42
|
const COPIAVEIS = new Set(['server.py', 'web', 'ferramentas', 'README.md', 'PRIMOCODE.md']);
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Onde está o Python 3 desta máquina.
|
|
46
|
+
*
|
|
47
|
+
* Procurar só por `python3` no PATH não basta, e isso custou uma tarefa
|
|
48
|
+
* inteira num Mac: o comando existe mas é o atalho da Apple, que sem as
|
|
49
|
+
* Ferramentas de Linha de Comando instaladas sai com erro em vez de versão —
|
|
50
|
+
* e o usuário lia "Python 3 não está instalado" com o Python instalado pelo
|
|
51
|
+
* Homebrew ali do lado, fora do PATH daquele terminal.
|
|
52
|
+
*
|
|
53
|
+
* Então: nomes comuns, versões numeradas, e os caminhos absolutos onde o
|
|
54
|
+
* Python realmente mora em cada sistema.
|
|
55
|
+
*/
|
|
44
56
|
function pythonCmd() {
|
|
45
|
-
const
|
|
57
|
+
const numeradas = ['python3.13', 'python3.12', 'python3.11', 'python3.10', 'python3.9'];
|
|
58
|
+
let candidatos;
|
|
59
|
+
if (os.platform() === 'win32') {
|
|
60
|
+
candidatos = ['python', 'py', 'python3', ...numeradas];
|
|
61
|
+
} else {
|
|
62
|
+
candidatos = [
|
|
63
|
+
'python3', 'python', ...numeradas,
|
|
64
|
+
// Homebrew (Apple Silicon e Intel) e o do sistema — o PATH de um
|
|
65
|
+
// terminal aberto por atalho às vezes não tem nenhum dos dois.
|
|
66
|
+
'/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3',
|
|
67
|
+
];
|
|
68
|
+
}
|
|
46
69
|
for (const c of candidatos) {
|
|
47
70
|
try {
|
|
48
71
|
const v = execSync(`${c} --version`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 }).toString();
|
|
@@ -130,10 +153,16 @@ async function garantirNoAr() {
|
|
|
130
153
|
if (!py) {
|
|
131
154
|
return {
|
|
132
155
|
ok: false,
|
|
133
|
-
error: 'O Studio precisa do Python 3,
|
|
156
|
+
error: 'O Studio precisa do Python 3, e não achei nenhum nesta máquina.',
|
|
134
157
|
comoResolver: os.platform() === 'darwin'
|
|
135
|
-
? '
|
|
136
|
-
|
|
158
|
+
? 'No Mac, o mais rápido é: xcode-select --install (traz o Python 3 da Apple). '
|
|
159
|
+
+ 'Ou, se você usa Homebrew: brew install python. Depois é só pedir de novo.'
|
|
160
|
+
: os.platform() === 'win32'
|
|
161
|
+
? 'Instale em python.org/downloads (marque "Add python.exe to PATH" no instalador).'
|
|
162
|
+
: 'Instale o Python 3 pelo gerenciador da sua distribuição (ex: sudo apt install python3).',
|
|
163
|
+
// Sem isto o agente responde "não consegui" e para. O trabalho
|
|
164
|
+
// pedido quase sempre tem um caminho que não depende do Studio.
|
|
165
|
+
enquantoIsso: 'Não desista da tarefa: dá para entregar em HTML com write_file e abrir com open_in_browser.',
|
|
137
166
|
};
|
|
138
167
|
}
|
|
139
168
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. 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/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|