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.
- package/bin/primocode.js +139 -51
- package/lib/act.js +10 -15
- package/lib/catalogo.js +1 -0
- package/lib/config.js +5 -2
- package/lib/desktop.js +4 -4
- package/lib/effort.js +51 -0
- package/lib/referencia.js +287 -0
- package/lib/tools.js +10 -0
- package/lib/ui.js +6 -6
- package/package.json +2 -2
- package/lib/streaming.js +0 -661
|
@@ -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(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<')
|
|
124
|
+
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/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 · /
|
|
160
|
+
'/help lista tudo · /effort ajusta o esforço · /status confere a API',
|
|
161
161
|
];
|
|
162
162
|
|
|
163
163
|
const NEWS = [
|
|
164
|
-
'/
|
|
165
|
-
'
|
|
166
|
-
'
|
|
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.
|
|
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"
|