primocode 7.15.0 → 7.17.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.
@@ -0,0 +1,287 @@
1
+ /**
2
+ * referencia.js — pesquisa de referência antes de escrever código.
3
+ *
4
+ * A ideia do dono: em vez de o agente inventar tudo da cabeça, ele consulta o
5
+ * que já existe sobre o pedido (documentação oficial, exemplos, respostas) e
6
+ * escreve com esse conhecimento na mão. A qualidade sobe porque ele para de
7
+ * chutar API que não existe e passa a seguir o padrão real da ferramenta.
8
+ *
9
+ * Duas decisões de projeto, deliberadas:
10
+ *
11
+ * 1. É VISÍVEL, não oculto. Cada pesquisa aparece no terminal com as fontes
12
+ * que ele abriu. Um agente que abre coisas escondido do dono da máquina é
13
+ * um agente em que não dá para confiar.
14
+ *
15
+ * 2. Ele extrai CONHECIMENTO (como se faz, qual API, que cuidado tomar), não
16
+ * blocos de código para colar. Copiar trechos de terceiros para dentro do
17
+ * projeto do usuário sem olhar licença é problema jurídico dele, não nosso;
18
+ * e código colado de fontes que não conversam entre si é justamente o que
19
+ * produz o Frankenstein que não compila. O modelo lê, aprende e ESCREVE.
20
+ *
21
+ * Zero dependências: https nativo. Sem chave de API — usa as fontes abertas.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const https = require('https');
27
+ const http = require('http');
28
+ const net = require('net');
29
+ const tls = require('tls');
30
+ const { URL } = require('url');
31
+
32
+ const TIMEOUT_MS = 12000;
33
+ const MAX_POR_FONTE = 4;
34
+ const MAX_TEXTO = 4500;
35
+
36
+ // Rede de empresa costuma exigir proxy. Sem isto, a pesquisa simplesmente não
37
+ // funciona em metade dos computadores corporativos — e o usuário nunca
38
+ // descobre por quê.
39
+ const PROXY = process.env.HTTPS_PROXY || process.env.https_proxy
40
+ || process.env.HTTP_PROXY || process.env.http_proxy || null;
41
+
42
+ /** Abre um túnel CONNECT no proxy e devolve o socket TLS pronto. */
43
+ function viaProxy(alvo, { timeout }) {
44
+ return new Promise((resolve, reject) => {
45
+ const p = new URL(PROXY);
46
+ const porta = alvo.port || 443;
47
+ const req = http.request({
48
+ host: p.hostname,
49
+ port: p.port || 80,
50
+ method: 'CONNECT',
51
+ path: `${alvo.hostname}:${porta}`,
52
+ timeout,
53
+ headers: p.username
54
+ ? { 'Proxy-Authorization': 'Basic ' + Buffer.from(`${p.username}:${p.password}`).toString('base64') }
55
+ : {},
56
+ });
57
+ req.on('connect', (res, socket) => {
58
+ if (res.statusCode !== 200) { socket.destroy(); return reject(new Error(`proxy ${res.statusCode}`)); }
59
+ resolve(tls.connect({ socket, servername: alvo.hostname }));
60
+ });
61
+ req.on('error', reject);
62
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout no proxy')); });
63
+ req.end();
64
+ });
65
+ }
66
+
67
+ function pegar(url, { timeout = TIMEOUT_MS } = {}) {
68
+ return new Promise((resolve) => {
69
+ let req;
70
+ const alvo = (() => { try { return new URL(url); } catch { return null; } })();
71
+ if (!alvo) return resolve(null);
72
+
73
+ const opcoes = {
74
+ headers: {
75
+ 'User-Agent': 'PrimoCode/1.0 (+https://github.com/ConectaPrimoAI/primocode)',
76
+ Accept: 'text/html,application/json;q=0.9,*/*;q=0.8',
77
+ },
78
+ timeout,
79
+ };
80
+
81
+ // Com proxy, a conexão vai por um túnel CONNECT; sem proxy, direto.
82
+ const abrir = PROXY && alvo.protocol === 'https:'
83
+ ? viaProxy(alvo, { timeout }).then((socket) => ({
84
+ ...opcoes, createConnection: () => socket, host: alvo.hostname, servername: alvo.hostname,
85
+ })).catch(() => null)
86
+ : Promise.resolve(opcoes);
87
+
88
+ abrir.then((cfg) => {
89
+ if (!cfg) return resolve(null);
90
+ try {
91
+ req = https.get(url, cfg, (res) => {
92
+ // Redirecionamento: segue uma vez, o suficiente para docs.
93
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
94
+ res.resume();
95
+ const destino = new URL(res.headers.location, url).href;
96
+ return pegar(destino, { timeout }).then(resolve);
97
+ }
98
+ if (res.statusCode !== 200) { res.resume(); return resolve(null); }
99
+ let corpo = '';
100
+ res.setEncoding('utf8');
101
+ res.on('data', (c) => {
102
+ corpo += c;
103
+ if (corpo.length > 400000) { req.destroy(); } // página gigante não serve
104
+ });
105
+ res.on('end', () => resolve(corpo));
106
+ res.on('error', () => resolve(null));
107
+ });
108
+ } catch { return resolve(null); }
109
+ req.on('timeout', () => { req.destroy(); resolve(null); });
110
+ req.on('error', () => resolve(null));
111
+ });
112
+ });
113
+ }
114
+
115
+ /** HTML → texto legível. Sem dependência: corta script/style e as tags. */
116
+ function textoDe(html) {
117
+ return String(html || '')
118
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
119
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
120
+ .replace(/<nav[\s\S]*?<\/nav>/gi, ' ')
121
+ .replace(/<footer[\s\S]*?<\/footer>/gi, ' ')
122
+ .replace(/<[^>]+>/g, ' ')
123
+ .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<')
124
+ .replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'")
125
+ .replace(/\s+/g, ' ')
126
+ .trim();
127
+ }
128
+
129
+ // Links que não são referência de nada — resultado de busca sujo.
130
+ const LIXO = /(facebook|twitter|x)\.com|instagram|pinterest|youtube\.com\/(?!watch)|\/search\?|duckduckgo|bing\.com\/(?!ck)|google\./i;
131
+
132
+ function limparLinks(pares, limite) {
133
+ const vistos = new Set();
134
+ const bons = [];
135
+ for (const { titulo, url } of pares) {
136
+ if (!/^https?:\/\//i.test(url) || LIXO.test(url)) continue;
137
+ const chave = url.replace(/[#?].*$/, '');
138
+ if (vistos.has(chave)) continue;
139
+ vistos.add(chave);
140
+ bons.push({ titulo: titulo.slice(0, 140), url });
141
+ if (bons.length >= limite) break;
142
+ }
143
+ return bons;
144
+ }
145
+
146
+ /** DuckDuckGo (HTML). O destino vem embrulhado em /l/?uddg=<url>. */
147
+ async function buscarDDG(consulta, limite) {
148
+ const html = await pegar(`https://duckduckgo.com/html/?q=${encodeURIComponent(consulta)}`);
149
+ if (!html) return [];
150
+ const pares = [];
151
+ const re = /<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
152
+ let m;
153
+ while ((m = re.exec(html))) {
154
+ let href = m[1];
155
+ const embrulho = href.match(/[?&]uddg=([^&]+)/);
156
+ if (embrulho) href = decodeURIComponent(embrulho[1]);
157
+ pares.push({ titulo: textoDe(m[2]), url: href });
158
+ }
159
+ return limparLinks(pares, limite);
160
+ }
161
+
162
+ /** Bing (HTML) — a reserva quando o DDG não responde. */
163
+ async function buscarBing(consulta, limite) {
164
+ const html = await pegar(`https://www.bing.com/search?q=${encodeURIComponent(consulta)}&setlang=en`);
165
+ if (!html) return [];
166
+ const pares = [];
167
+ const re = /<h2>\s*<a[^>]+href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
168
+ let m;
169
+ while ((m = re.exec(html))) pares.push({ titulo: textoDe(m[2]), url: m[1] });
170
+ return limparLinks(pares, limite);
171
+ }
172
+
173
+ /** SearXNG público, em JSON — a terceira porta. */
174
+ async function buscarSearx(consulta, limite) {
175
+ const corpo = await pegar(`https://searx.be/search?q=${encodeURIComponent(consulta)}&format=json`);
176
+ if (!corpo) return [];
177
+ try {
178
+ const j = JSON.parse(corpo);
179
+ return limparLinks((j.results || []).map((r) => ({ titulo: String(r.title || ''), url: String(r.url || '') })), limite);
180
+ } catch { return []; }
181
+ }
182
+
183
+ /**
184
+ * Busca na web. Tenta as portas em ordem e para na primeira que responder —
185
+ * um buscador fora do ar (ou bloqueado pela rede) não pode desligar a
186
+ * pesquisa inteira. Sem chave de API em nenhuma delas.
187
+ */
188
+ async function buscar(consulta, limite = MAX_POR_FONTE) {
189
+ for (const porta of [buscarDDG, buscarBing, buscarSearx]) {
190
+ try {
191
+ const achados = await porta(consulta, limite);
192
+ if (achados.length) return achados;
193
+ } catch {}
194
+ }
195
+ return [];
196
+ }
197
+
198
+ /** Respostas do StackOverflow — a API pública, sem chave. */
199
+ async function stackoverflow(consulta, limite = 3) {
200
+ const url = 'https://api.stackexchange.com/2.3/search/advanced'
201
+ + `?order=desc&sort=relevance&q=${encodeURIComponent(consulta)}`
202
+ + '&site=stackoverflow&filter=default&pagesize=' + limite;
203
+ const corpo = await pegar(url);
204
+ if (!corpo) return [];
205
+ try {
206
+ const j = JSON.parse(corpo);
207
+ return (j.items || []).slice(0, limite).map((i) => ({
208
+ titulo: textoDe(i.title).slice(0, 140),
209
+ url: i.link,
210
+ nota: `${i.score} votos${i.is_answered ? ', respondida' : ''}`,
211
+ }));
212
+ } catch { return []; }
213
+ }
214
+
215
+ // Última porta: a documentação oficial dos assuntos mais comuns, direto. Não
216
+ // depende de buscador nenhum — é o que salva a pesquisa em rede corporativa
217
+ // que bloqueia mecanismo de busca mas libera site de documentação.
218
+ const DOCS = [
219
+ [/\b(js|javascript|array|string|promise|fetch|dom|css|html|regex)\b/i,
220
+ (q) => `https://developer.mozilla.org/en-US/search?q=${encodeURIComponent(q)}`],
221
+ [/\b(react|jsx|hook|usestate|useeffect)\b/i, () => 'https://react.dev/reference/react'],
222
+ [/\b(node|npm|express|fs|require)\b/i, () => 'https://nodejs.org/docs/latest/api/'],
223
+ [/\b(python|django|flask|pandas)\b/i,
224
+ (q) => `https://docs.python.org/3/search.html?q=${encodeURIComponent(q)}`],
225
+ ];
226
+
227
+ function docsConhecidas(consulta) {
228
+ const achados = [];
229
+ for (const [re, url] of DOCS) {
230
+ if (re.test(consulta)) achados.push({ titulo: 'documentação oficial', url: url(consulta) });
231
+ }
232
+ return achados;
233
+ }
234
+
235
+ /**
236
+ * A pesquisa completa: busca, abre as melhores páginas e devolve o texto
237
+ * delas para o modelo LER. O modelo escreve o código dele com isso na mão.
238
+ */
239
+ async function pesquisar({ consulta, paginas = 3 }) {
240
+ const termo = String(consulta || '').trim();
241
+ if (!termo) return { ok: false, error: 'diga o que pesquisar' };
242
+
243
+ const [web, so] = await Promise.all([
244
+ buscar(termo, MAX_POR_FONTE),
245
+ stackoverflow(termo, 2),
246
+ ]);
247
+
248
+ // A documentação oficial entra SEMPRE na fila, não só quando as outras
249
+ // portas vieram vazias: ela é a melhor fonte, e é o que salva a pesquisa
250
+ // quando os links do buscador não abrem (rede corporativa, site fora do
251
+ // ar, bloqueio). Foi assim que a pesquisa falhava mesmo achando links.
252
+ const candidatos = [...web, ...so, ...docsConhecidas(termo)]
253
+ .slice(0, Math.max(2, Math.min(8, paginas + 4)));
254
+ if (!candidatos.length) {
255
+ return { ok: false, error: `nada encontrado para "${termo}" (sem internet?)`, consulta: termo };
256
+ }
257
+
258
+ const lidas = [];
259
+ for (const cand of candidatos) {
260
+ if (lidas.length >= paginas) break;
261
+ const html = await pegar(cand.url);
262
+ if (!html) continue;
263
+ const texto = textoDe(html);
264
+ if (texto.length < 200) continue;
265
+ lidas.push({
266
+ titulo: cand.titulo,
267
+ url: cand.url,
268
+ nota: cand.nota,
269
+ trecho: texto.slice(0, MAX_TEXTO),
270
+ });
271
+ }
272
+
273
+ if (!lidas.length) {
274
+ return { ok: false, error: 'achei os links mas não consegui abrir nenhum', consulta: termo, links: candidatos.map((c) => c.url) };
275
+ }
276
+
277
+ return {
278
+ ok: true,
279
+ consulta: termo,
280
+ fontes: lidas.length,
281
+ referencias: lidas,
282
+ comoUsar: 'Leia, entenda o padrão e ESCREVA o seu próprio código. Não copie trechos literais: '
283
+ + 'cite a fonte se aproveitar uma ideia, e confira se a API citada existe na versão que o projeto usa.',
284
+ };
285
+ }
286
+
287
+ module.exports = { pesquisar, buscar, stackoverflow, textoDe };
package/lib/tools.js CHANGED
@@ -13,6 +13,7 @@ const os = require('os');
13
13
  const { exec } = require('child_process');
14
14
  const browser = require('./browser');
15
15
  const desktop = require('./desktop');
16
+ const referencia = require('./referencia');
16
17
 
17
18
  const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
18
19
  const MAX_ARQUIVOS_VARREDURA = 5000;
@@ -388,6 +389,15 @@ async function despachar(name, args, ctx) {
388
389
  case 'write_todos':
389
390
  return writeTodos(args);
390
391
 
392
+ // Pesquisa de referência: abre documentação e exemplos sobre o
393
+ // pedido e devolve o TEXTO para o modelo aprender antes de
394
+ // escrever. Aparece no terminal, com as fontes — nada oculto.
395
+ case 'pesquisar_referencia':
396
+ return referencia.pesquisar({
397
+ consulta: args.consulta || args.query || '',
398
+ paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
399
+ });
400
+
391
401
  // ── Navegador controlado (CDP) — o agente age numa página real ──
392
402
  case 'browser_open': {
393
403
  const alvo = /^https?:\/\//i.test(args.url || '')
package/lib/ui.js CHANGED
@@ -157,13 +157,13 @@ const NOME = 'Primo Code';
157
157
  const TIPS = [
158
158
  'Peça um app novo, ou aponte para um repositório e mande clonar',
159
159
  '/dir <pasta> para ele trabalhar numa pasta que já é sua',
160
- '/help lista tudo · /mode troca o modo · /status confere a API',
160
+ '/help lista tudo · /effort ajusta o esforço · /status confere a API',
161
161
  ];
162
162
 
163
163
  const NEWS = [
164
- '/streaming agora é VÍDEO ao vivo — assista ele trabalhando, aba por aba',
165
- 'Comandos se autopreenchem: /str + Tab (ou Enter) vira /streaming',
166
- 'Ele INTERAGE com o computador: janelas, botões e ícones pelo nome',
164
+ '/effort escolhe o esforço num slider — do rápido ao mais esperto',
165
+ 'Do "high" para cima ele PESQUISA referências antes de escrever',
166
+ 'Esc Esc a qualquer momento para o agente no meio do trabalho',
167
167
  ];
168
168
 
169
169
  // ── Cabeçalho ────────────────────────────────────────────────────────────
@@ -302,7 +302,7 @@ function actionHeader(name, args) {
302
302
  read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
303
303
  list_dir:'List', glob:'Glob', grep:'Grep', run_shell:'Bash',
304
304
  run_in_new_terminal:'Terminal', open_in_browser:'Open', spawn_agent:'Agent',
305
- write_todos:'Plano', finish:'Finish',
305
+ write_todos:'Plano', finish:'Finish', pesquisar_referencia:'Pesquisa',
306
306
  browser_open:'Browser', browser_click:'Browser·click', browser_type:'Browser·type',
307
307
  browser_key:'Browser·key', browser_read:'Browser·read', browser_screenshot:'Browser·shot',
308
308
  browser_scroll:'Browser·scroll', browser_wait:'Browser·wait', browser_eval:'Browser·eval',
@@ -314,7 +314,7 @@ function actionHeader(name, args) {
314
314
  desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
315
315
  };
316
316
  const label = nice[name] || name;
317
- const argStr = args ? (args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
317
+ const argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
318
318
  return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
319
319
  }
320
320
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "7.15.0",
3
+ "version": "7.17.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"
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"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"