primocode 7.7.0 → 7.9.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/README.md +25 -5
- package/bin/primocode.js +145 -33
- package/lib/act.js +31 -10
- package/lib/browser.js +182 -14
- package/lib/catalogo.js +94 -0
- package/lib/pasta.js +187 -0
- package/lib/tools.js +31 -3
- package/lib/ui.js +24 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
# PrimoCode v7.
|
|
1
|
+
# PrimoCode v7.9.0
|
|
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** —
|
|
5
5
|
e lembra do que fez entre uma sessão e outra.
|
|
6
6
|
|
|
7
|
+
**Grátis, sem chave de API sua e sem teto de gasto.** Os modelos são os grátis do
|
|
8
|
+
OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um modelo
|
|
9
|
+
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
|
+
|
|
7
11
|
```
|
|
8
|
-
▐▛███▜▌ Primo Code v7.
|
|
12
|
+
▐▛███▜▌ Primo Code v7.9.0
|
|
9
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
10
|
-
▘▘ ▝▝ ~/
|
|
14
|
+
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
11
15
|
```
|
|
12
16
|
|
|
13
17
|
Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
|
|
@@ -119,12 +123,27 @@ para o agente custa token e pode escrever arquivo que ninguém pediu.
|
|
|
119
123
|
|
|
120
124
|
## Onde ele trabalha
|
|
121
125
|
|
|
126
|
+
**Por padrão, sempre `~/primocode`** — não importa de onde você abriu o
|
|
127
|
+
PrimoCode. Antes ele adotava a pasta atual do terminal, e o mesmo "crie um site"
|
|
128
|
+
caía num lugar diferente a cada vez; quem digitava não tinha como saber onde ia
|
|
129
|
+
parar. Agora o começo é sempre o mesmo, e o cabeçalho avisa em amber qual é.
|
|
130
|
+
|
|
131
|
+
Para trabalhar em outra pasta, três caminhos:
|
|
132
|
+
|
|
122
133
|
| Situação | Pasta de trabalho |
|
|
123
134
|
|---|---|
|
|
124
|
-
|
|
|
135
|
+
| **Falar o nome dela no pedido** — "crie o index.html na pasta loja-do-joao" | ele procura no seu computador (`~/primocode`, Downloads, Documentos, Desktop, projetos…) e muda sozinho |
|
|
125
136
|
| `/dir ~/meus-projetos/site` | a pasta que **você** escolher, onde ela estiver |
|
|
126
137
|
| `/project loja` | cria `~/primocode/loja`, com memória própria |
|
|
127
138
|
|
|
139
|
+
A busca por nome é conservadora de propósito: se achar duas pastas com o mesmo
|
|
140
|
+
nome, ela **lista e espera** o `/dir` em vez de escolher — heurística não deve
|
|
141
|
+
decidir onde um agente escreve arquivo. E nome de arquivo nunca é confundido com
|
|
142
|
+
pasta: "edita o `src/app.js`" não muda pasta nenhuma.
|
|
143
|
+
|
|
144
|
+
**Pasta de projeto nova só nasce quando ele vai mexer em arquivo.** Pedir "abre o
|
|
145
|
+
Chrome" não cria projeto — antes criava, e `~/primocode` enchia de pasta vazia.
|
|
146
|
+
|
|
128
147
|
`/dir` sem argumento mostra a pasta ativa e de onde ela veio. Caminho relativo
|
|
129
148
|
resolve contra a pasta ativa, então depois de um `/dir` o `/dir src` entra na
|
|
130
149
|
subpasta. Se a pasta não existir, ele pergunta antes de criar; se for a sua home
|
|
@@ -241,9 +260,10 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
|
|
|
241
260
|
| `/dir <caminho>` | Trabalha numa pasta que já existe, onde você quiser |
|
|
242
261
|
| `/project` | Lista projetos. `/project <nome>` cria em `~/primocode` |
|
|
243
262
|
| `/mode primocode\|chat\|auto` | Alterna o modo (fica guardado) |
|
|
244
|
-
| `/model top\|main\|fast` | top =
|
|
263
|
+
| `/model top\|main\|fast` | **Todos grátis.** top = o mais capaz (550B, 1M de contexto), main = agente de código (padrão), fast = o mais rápido. Aceita qualquer ID do OpenRouter |
|
|
245
264
|
| `/attach <arquivo>` | Anexa arquivo ao próximo pedido |
|
|
246
265
|
| `/detach` · `/files` | Remove / lista anexos |
|
|
266
|
+
| `/tudo` | Lista as 30 ferramentas que ele sabe usar, por grupo |
|
|
247
267
|
| `/tree [n]` | Árvore do projeto atual |
|
|
248
268
|
| `/read <arquivo>` | Mostra um arquivo do projeto |
|
|
249
269
|
| `/run <comando>` | Executa no shell, dentro da pasta do projeto |
|
package/bin/primocode.js
CHANGED
|
@@ -20,6 +20,13 @@ const api = require('../lib/api');
|
|
|
20
20
|
const tools = require('../lib/tools');
|
|
21
21
|
const cfg = require('../lib/config');
|
|
22
22
|
const { runAct } = require('../lib/act');
|
|
23
|
+
const pasta = require('../lib/pasta');
|
|
24
|
+
const catalogo = require('../lib/catalogo');
|
|
25
|
+
|
|
26
|
+
// A pasta de onde o comando foi dado. Não é mais onde ele trabalha (o padrão é
|
|
27
|
+
// ~/primocode), mas continua servindo de ponto de partida para achar pasta que
|
|
28
|
+
// o usuário mencionar por nome relativo.
|
|
29
|
+
const CWD_INICIAL = process.cwd();
|
|
23
30
|
|
|
24
31
|
const VERSION = pkg.version;
|
|
25
32
|
|
|
@@ -32,7 +39,7 @@ const state = {
|
|
|
32
39
|
lastResponse: '',
|
|
33
40
|
currentProject: null,
|
|
34
41
|
workDir: null, // pasta onde o agente trabalha nesta sessão
|
|
35
|
-
workDirOrigem: null, // '
|
|
42
|
+
workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
|
|
36
43
|
};
|
|
37
44
|
|
|
38
45
|
const HELP = `
|
|
@@ -43,6 +50,7 @@ ${c.bold('Comandos')}
|
|
|
43
50
|
${c.brand('/model')} /model top|main|fast — ou um ID do OpenRouter
|
|
44
51
|
${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
|
|
45
52
|
${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
|
|
53
|
+
${c.brand('/tudo')} lista tudo que ele sabe fazer
|
|
46
54
|
${c.brand('/tree')} [n] árvore do projeto
|
|
47
55
|
${c.brand('/read')} <arquivo> mostra um arquivo
|
|
48
56
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
@@ -61,8 +69,11 @@ ${c.bold('Modos')}
|
|
|
61
69
|
${c.brand('Auto')} ${c.muted('Escolhe por pedido: conversa vai pro chat, tarefa vai pro agente')}
|
|
62
70
|
|
|
63
71
|
${c.bold('Onde ele trabalha')}
|
|
64
|
-
${c.muted('
|
|
65
|
-
${c.muted('pasta
|
|
72
|
+
${c.muted('Sempre em')} ${c.warn('~/primocode')}${c.muted(', não importa de onde você abriu o PrimoCode.')}
|
|
73
|
+
${c.muted('Para outra pasta, dois caminhos:')}
|
|
74
|
+
${c.muted(' ·')} ${c.brand('/dir <caminho>')} ${c.muted('aponta para qualquer pasta sua')}
|
|
75
|
+
${c.muted(' ·')} ${c.muted('falar o nome dela no pedido — ele procura no seu computador')}
|
|
76
|
+
${c.dim('Pasta de projeto nova só nasce quando ele vai mexer em arquivo.')}
|
|
66
77
|
`;
|
|
67
78
|
|
|
68
79
|
let rl = null;
|
|
@@ -120,26 +131,34 @@ function deriveProjectName(text) {
|
|
|
120
131
|
return picked || 'projeto';
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
134
|
+
// Pasta em que é seguro deixar um agente criar e editar arquivo. A home e a raiz
|
|
135
|
+
// do disco não contam — ninguém quer um agente soltando arquivo no meio de
|
|
136
|
+
// C:\Users\você. O ~/primocode ERA recusado aqui, de quando a pasta atual do
|
|
137
|
+
// terminal era adotada; hoje ele é o padrão, então recusá-lo não faz sentido.
|
|
138
|
+
function pastaSegura(dir) {
|
|
127
139
|
const home = os.homedir();
|
|
128
140
|
const resolved = path.resolve(dir);
|
|
129
141
|
if (resolved === path.resolve(home)) return false;
|
|
130
142
|
if (resolved === path.parse(resolved).root) return false;
|
|
131
|
-
if (resolved.startsWith(path.join(home, 'primocode'))) return false;
|
|
132
143
|
return true;
|
|
133
144
|
}
|
|
134
145
|
|
|
135
|
-
// Escolhe onde trabalhar SEM criar nada.
|
|
136
|
-
//
|
|
146
|
+
// Escolhe onde trabalhar SEM criar nada.
|
|
147
|
+
//
|
|
148
|
+
// Três casos, nesta ordem:
|
|
149
|
+
// 1. pasta escolhida a dedo (/dir, ou detectada no pedido) — manda, sempre.
|
|
150
|
+
// 2. projeto que JÁ existe e parece com o pedido — entra nele agora, senão a
|
|
151
|
+
// memory.md dele não carrega neste turno.
|
|
152
|
+
// 3. projeto novo — fica PENDENTE: age em ~/primocode e só cria a pasta se o
|
|
153
|
+
// agente for mexer em arquivo. Pedir para abrir o Chrome não cria projeto.
|
|
137
154
|
function planWorkDir(userText) {
|
|
138
|
-
if (state.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
155
|
+
if (['/dir', 'detectada', 'projeto'].includes(state.workDirOrigem)) {
|
|
156
|
+
return { dir: state.workDir, pending: false, name: state.currentProject };
|
|
157
|
+
}
|
|
158
|
+
const proposto = deriveProjectName(userText);
|
|
159
|
+
const parecido = cfg.findSimilarProject(proposto);
|
|
160
|
+
if (parecido) return { dir: parecido.dir, pending: false, name: parecido.name, similar: true };
|
|
161
|
+
return { dir: cfg.ROOT_DIR, projeto: cfg.getProjectDir(proposto), pending: true, name: proposto };
|
|
143
162
|
}
|
|
144
163
|
|
|
145
164
|
function createPendingProject(plan) {
|
|
@@ -159,6 +178,63 @@ function projectDir() {
|
|
|
159
178
|
|
|
160
179
|
// ── Envio ──────────────────────────────────────────────────────────────
|
|
161
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Falou de uma pasta? Ele procura no computador e muda para ela.
|
|
183
|
+
*
|
|
184
|
+
* Conservador de propósito: só troca quando acha UMA pasta. Com mais de uma,
|
|
185
|
+
* lista e espera o /dir — heurística não deve escolher onde o agente escreve.
|
|
186
|
+
*/
|
|
187
|
+
function talvezTrocarDePasta(userText) {
|
|
188
|
+
const candidato = pasta.detectarPasta(userText);
|
|
189
|
+
if (!candidato) return;
|
|
190
|
+
|
|
191
|
+
const achado = pasta.resolverPasta(candidato, { raiz: cfg.ROOT_DIR, cwd: CWD_INICIAL });
|
|
192
|
+
if (!achado) return; // não existe no disco: segue onde está, sem barulho
|
|
193
|
+
|
|
194
|
+
if (achado.ambiguo) {
|
|
195
|
+
console.log(c.warn(` Achei ${achado.ambiguo.length} pastas chamadas "${candidato}":`));
|
|
196
|
+
achado.ambiguo.forEach((a) => console.log(` ${c.dim('·')} ${c.white(shortPath(a.caminho))}`));
|
|
197
|
+
console.log(c.dim(' Escolha com /dir <caminho>.'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (path.resolve(achado.caminho) === path.resolve(projectDir())) return; // já está lá
|
|
201
|
+
if (!pastaSegura(achado.caminho)) return; // home ou raiz: só por /dir explícito
|
|
202
|
+
|
|
203
|
+
state.workDir = achado.caminho;
|
|
204
|
+
state.currentProject = path.basename(achado.caminho);
|
|
205
|
+
state.workDirOrigem = 'detectada';
|
|
206
|
+
console.log(c.ok(` Trabalhando em ${shortPath(achado.caminho)}`) + c.dim(` (você falou "${candidato}")`));
|
|
207
|
+
refreshPrompt();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Decide, de uma vez, onde este pedido vai trabalhar.
|
|
212
|
+
*
|
|
213
|
+
* ESTA É A ÚNICA função que decide pasta. Existiam duas cópias — uma no
|
|
214
|
+
* sendPrompt e outra no modo um-tiro — e a do um-tiro ficou sem a detecção: pedir
|
|
215
|
+
* `primocode "crie x na pasta loja"` ignorava a pasta e criava projeto novo.
|
|
216
|
+
* Duas cópias da mesma decisão significam que uma delas está errada.
|
|
217
|
+
*/
|
|
218
|
+
function prepararPasta(userText) {
|
|
219
|
+
// A pasta é decidida ANTES de o agente começar. Trocar no meio faria as
|
|
220
|
+
// primeiras tools agirem num lugar e as seguintes em outro.
|
|
221
|
+
talvezTrocarDePasta(userText);
|
|
222
|
+
|
|
223
|
+
const plan = planWorkDir(userText);
|
|
224
|
+
|
|
225
|
+
// Projeto que já existe: entra nele AGORA. Se deixasse para depois, o
|
|
226
|
+
// buildMemoryContext deste turno leria a pasta errada e a memória do projeto
|
|
227
|
+
// não carregaria.
|
|
228
|
+
if (!plan.pending && path.resolve(plan.dir) !== path.resolve(projectDir())) {
|
|
229
|
+
state.workDir = plan.dir;
|
|
230
|
+
state.currentProject = plan.name || path.basename(plan.dir);
|
|
231
|
+
state.workDirOrigem = 'projeto';
|
|
232
|
+
console.log(c.ok(` Continuando em ${state.currentProject}`) + c.muted(` (${shortPath(plan.dir)})`));
|
|
233
|
+
refreshPrompt();
|
|
234
|
+
}
|
|
235
|
+
return plan;
|
|
236
|
+
}
|
|
237
|
+
|
|
162
238
|
async function sendPrompt(userText) {
|
|
163
239
|
let prompt = userText;
|
|
164
240
|
if (state.attachedFiles.length) {
|
|
@@ -179,15 +255,17 @@ async function sendPrompt(userText) {
|
|
|
179
255
|
}
|
|
180
256
|
|
|
181
257
|
if (modo === 'primocode') {
|
|
182
|
-
const plan =
|
|
258
|
+
const plan = prepararPasta(userText);
|
|
259
|
+
|
|
183
260
|
const r = await runAct(prompt, {
|
|
184
261
|
server: state.config.server,
|
|
185
262
|
model: state.config.model,
|
|
186
263
|
token: state.config.token,
|
|
187
264
|
history: state.history,
|
|
188
265
|
projectDir: plan.dir,
|
|
189
|
-
projectName: plan.
|
|
190
|
-
// Só chamado se o agente for mesmo
|
|
266
|
+
projectName: plan.name || state.currentProject,
|
|
267
|
+
// Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
|
|
268
|
+
// não é motivo para criar pasta de projeto.
|
|
191
269
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
192
270
|
});
|
|
193
271
|
state.history.push({ role: 'user', content: userText });
|
|
@@ -274,7 +352,12 @@ async function handleSlash(text) {
|
|
|
274
352
|
case 'cd':
|
|
275
353
|
case 'pasta': {
|
|
276
354
|
if (!rest) {
|
|
277
|
-
const de = {
|
|
355
|
+
const de = {
|
|
356
|
+
padrao: 'a pasta padrão do PrimoCode',
|
|
357
|
+
'/dir': 'você escolheu com /dir',
|
|
358
|
+
detectada: 'você falou dela no pedido',
|
|
359
|
+
projeto: 'projeto do PrimoCode',
|
|
360
|
+
};
|
|
278
361
|
console.log(` ${c.white(shortPath(projectDir()))}`);
|
|
279
362
|
console.log(c.muted(` ${de[state.workDirOrigem] || 'pasta atual do terminal'}`));
|
|
280
363
|
console.log(c.dim(' /dir <caminho> muda · /project <nome> cria uma nova em ~/primocode'));
|
|
@@ -301,7 +384,7 @@ async function handleSlash(text) {
|
|
|
301
384
|
// A home e a raiz do disco são recusadas por padrão em qualquer
|
|
302
385
|
// lugar do código — aqui o pedido é explícito, então pergunta em vez
|
|
303
386
|
// de recusar, mas pergunta mesmo: é onde um write_file erra feio.
|
|
304
|
-
if (!
|
|
387
|
+
if (!pastaSegura(alvo)) {
|
|
305
388
|
console.log(c.warn(` ${shortPath(alvo)} é a sua home ou a raiz do disco.`));
|
|
306
389
|
if (!(await confirm('O agente pode criar e editar arquivos aí. Confirma?'))) {
|
|
307
390
|
console.log(c.muted(' Nada mudou.'));
|
|
@@ -334,15 +417,16 @@ async function handleSlash(text) {
|
|
|
334
417
|
|
|
335
418
|
case 'model': {
|
|
336
419
|
const m = rest.toLowerCase();
|
|
420
|
+
// Todos grátis. A lista viva de cada corrente está no /status.
|
|
337
421
|
const nomes = {
|
|
338
|
-
top: '
|
|
339
|
-
main: '
|
|
340
|
-
fast: '
|
|
422
|
+
top: 'o mais capaz — 550B, 1 milhão de tokens de contexto',
|
|
423
|
+
main: 'o padrão — agente de código, o melhor em tarefa de terminal',
|
|
424
|
+
fast: 'o mais rápido — resposta curta, alta vazão',
|
|
341
425
|
};
|
|
342
426
|
if (nomes[m]) {
|
|
343
427
|
state.config.model = m;
|
|
344
428
|
cfg.saveConfig({ model: m });
|
|
345
|
-
console.log(c.ok(`Modelo: ${m}`) + c.muted(` ${nomes[m]}`));
|
|
429
|
+
console.log(c.ok(`Modelo: ${m}`) + c.muted(` ${nomes[m]}`) + c.dim(' · grátis'));
|
|
346
430
|
} else if (m.includes('/')) {
|
|
347
431
|
// ID literal do OpenRouter, para quem quer sair dos três.
|
|
348
432
|
state.config.model = m;
|
|
@@ -370,6 +454,25 @@ async function handleSlash(text) {
|
|
|
370
454
|
else state.attachedFiles.forEach((f) => console.log(` ${c.brand(f.path)} ${c.muted(tools.fmtBytes(f.content.length))}`));
|
|
371
455
|
break;
|
|
372
456
|
|
|
457
|
+
// A lista vem de lib/catalogo.js, e o test/catalogo.test.js cruza aquela
|
|
458
|
+
// lista com as tools que o tools.execute realmente atende. Ou seja: esta
|
|
459
|
+
// tela não consegue prometer coisa que o programa não faz.
|
|
460
|
+
case 'tudo':
|
|
461
|
+
case 'tools':
|
|
462
|
+
case 'capacidades': {
|
|
463
|
+
const cols = Math.max(40, Math.min(process.stdout.columns || 90, 110));
|
|
464
|
+
console.log('');
|
|
465
|
+
for (const g of catalogo.GRUPOS) {
|
|
466
|
+
console.log(' ' + c.bold(c.white(g.nome)) + ' ' + ui.truncate(c.dim('— ' + g.resumo), cols - 4 - ui.visibleWidth(g.nome)));
|
|
467
|
+
for (const i of g.itens) {
|
|
468
|
+
console.log(' ' + c.brand(ui.padRight(i.tool, 21)) + ui.truncate(c.muted(i.faz), cols - 26));
|
|
469
|
+
}
|
|
470
|
+
console.log('');
|
|
471
|
+
}
|
|
472
|
+
console.log(c.dim(` ${catalogo.TODAS.length} ferramentas. Você não precisa saber nenhum desses nomes — peça em português.`));
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
|
|
373
476
|
case 'tree': console.log(tools.treeDir(projectDir(), parseInt(rest, 10) || 2)); break;
|
|
374
477
|
|
|
375
478
|
case 'read': {
|
|
@@ -451,7 +554,13 @@ async function handleSlash(text) {
|
|
|
451
554
|
sp.stop();
|
|
452
555
|
console.log(c.ok(`Servidor: ${h.status || 'ok'}`));
|
|
453
556
|
if (h.version) console.log(c.muted(` Versão: ${h.version}`));
|
|
454
|
-
if (h.model) console.log(c.muted(` Modelo: ${h.model}`));
|
|
557
|
+
if (h.model) console.log(c.muted(` Modelo: ${h.model}`) + (h.gratis ? c.dim(' · grátis') : ''));
|
|
558
|
+
// A corrente inteira: é o que garante que um modelo no limite não
|
|
559
|
+
// vira erro na sua tela.
|
|
560
|
+
if (h.correntes && h.correntes[state.config.model || 'main']) {
|
|
561
|
+
const fila = h.correntes[state.config.model] || h.correntes.main;
|
|
562
|
+
console.log(c.dim(` Reservas: ${fila.length - 1}`));
|
|
563
|
+
}
|
|
455
564
|
} catch (e) { sp.stop(); console.log(c.err(`Indisponível: ${e.message}`)); }
|
|
456
565
|
break;
|
|
457
566
|
}
|
|
@@ -485,12 +594,12 @@ function shortPath(p) {
|
|
|
485
594
|
async function main() {
|
|
486
595
|
cfg.ensureStructure();
|
|
487
596
|
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
597
|
+
// Padrão fixo: ~/primocode, não importa de onde o comando foi dado. Antes ele
|
|
598
|
+
// adotava a pasta atual do terminal, e o mesmo "crie um site" caía num lugar
|
|
599
|
+
// diferente a cada vez — quem digitava não tinha como saber onde ia parar.
|
|
600
|
+
state.workDir = cfg.ROOT_DIR;
|
|
601
|
+
state.currentProject = null;
|
|
602
|
+
state.workDirOrigem = 'padrao';
|
|
494
603
|
|
|
495
604
|
// Modo guardado entre sessões. O padrão é auto: sem ele, um "oi" gasta o
|
|
496
605
|
// mesmo que uma refatoração.
|
|
@@ -523,14 +632,16 @@ async function main() {
|
|
|
523
632
|
} catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
|
|
524
633
|
}
|
|
525
634
|
|
|
526
|
-
|
|
635
|
+
// Mesma função do modo interativo: detecta a pasta, adota projeto que já
|
|
636
|
+
// existe, e só então decide se há projeto novo pendente.
|
|
637
|
+
const plan = prepararPasta(promptText);
|
|
527
638
|
await runAct(fullPrompt, {
|
|
528
639
|
server: state.config.server,
|
|
529
640
|
model: state.config.model,
|
|
530
641
|
token: state.config.token,
|
|
531
642
|
history: [],
|
|
532
643
|
projectDir: plan.dir,
|
|
533
|
-
projectName: plan.
|
|
644
|
+
projectName: plan.name || state.currentProject,
|
|
534
645
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
535
646
|
});
|
|
536
647
|
process.exit(0);
|
|
@@ -540,6 +651,7 @@ async function main() {
|
|
|
540
651
|
modo: state.mode,
|
|
541
652
|
mascot: state.config.mascot,
|
|
542
653
|
userName: state.config.userName,
|
|
654
|
+
pastaPadrao: state.workDirOrigem === 'padrao',
|
|
543
655
|
}));
|
|
544
656
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
|
|
545
657
|
rl.prompt();
|
package/lib/act.js
CHANGED
|
@@ -14,6 +14,7 @@ const { c, actionHeader, Spinner, friendlyError } = require('./ui');
|
|
|
14
14
|
const tools = require('./tools');
|
|
15
15
|
const api = require('./api');
|
|
16
16
|
const cfg = require('./config');
|
|
17
|
+
const catalogo = require('./catalogo');
|
|
17
18
|
|
|
18
19
|
const MAX_ITERATIONS = 15;
|
|
19
20
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
@@ -136,9 +137,13 @@ async function runAct(userPrompt, opts) {
|
|
|
136
137
|
ensureWorkDir, profundidade = 0, subagent = false,
|
|
137
138
|
} = opts || {};
|
|
138
139
|
|
|
139
|
-
// A pasta só nasce quando o agente for mesmo
|
|
140
|
-
//
|
|
141
|
-
|
|
140
|
+
// A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
|
|
141
|
+
// Antes ela nascia na primeira tool qualquer — pedir "abre o Chrome" criava
|
|
142
|
+
// um projeto vazio, e ~/primocode enchia de pasta que não era projeto de
|
|
143
|
+
// nada. Quem decide é o catálogo: `escreve: true`.
|
|
144
|
+
let criarPasta = ensureWorkDir || null;
|
|
145
|
+
let pastaAtual = projectDir; // onde as tools agem AGORA
|
|
146
|
+
let temPasta = !criarPasta; // já é a pasta definitiva?
|
|
142
147
|
const falhasPendentes = [];
|
|
143
148
|
let tentativasDeEncerrar = 0;
|
|
144
149
|
|
|
@@ -235,16 +240,21 @@ async function runAct(userPrompt, opts) {
|
|
|
235
240
|
|
|
236
241
|
if (tc.name === 'spawn_agent') {
|
|
237
242
|
const r = await runSubagent(tc.arguments || {}, {
|
|
238
|
-
server, model, token, projectDir, projectName, profundidade,
|
|
243
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade,
|
|
239
244
|
});
|
|
240
245
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
241
246
|
continue;
|
|
242
247
|
}
|
|
243
248
|
|
|
244
|
-
if (
|
|
249
|
+
if (criarPasta && catalogo.escreveArquivo(tc.name)) {
|
|
250
|
+
const nova = criarPasta();
|
|
251
|
+
if (nova) pastaAtual = nova;
|
|
252
|
+
criarPasta = null;
|
|
253
|
+
temPasta = true;
|
|
254
|
+
}
|
|
245
255
|
|
|
246
256
|
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
247
|
-
const result = await tools.execute(tc.name, tc.arguments, { projectDir });
|
|
257
|
+
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
248
258
|
logResult(tc.name, result, profundidade);
|
|
249
259
|
|
|
250
260
|
// Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
|
|
@@ -261,11 +271,22 @@ async function runAct(userPrompt, opts) {
|
|
|
261
271
|
}
|
|
262
272
|
}
|
|
263
273
|
|
|
264
|
-
|
|
274
|
+
// Nunca terminar em silêncio. Quando o modelo devolvia resposta vazia sem
|
|
275
|
+
// chamar tool, o usuário via a tela parar e mais nada — pior que erro, porque
|
|
276
|
+
// não dá nem para saber que acabou. Medido: um pedido de navegador morria assim
|
|
277
|
+
// depois de a primeira tentativa de seletor falhar.
|
|
278
|
+
if (!finalText) {
|
|
279
|
+
finalText = falhasPendentes.length
|
|
280
|
+
? `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1]}`
|
|
281
|
+
: 'Parei sem concluir e sem explicação do modelo. Tente pedir de novo, em partes menores.';
|
|
282
|
+
if (!subagent) console.log('\n' + c.warn(finalText));
|
|
283
|
+
}
|
|
265
284
|
|
|
266
|
-
// Nada de arquivo mexido = nada para lembrar.
|
|
267
|
-
|
|
268
|
-
|
|
285
|
+
// Nada de arquivo mexido = nada para lembrar. E grava em `pastaAtual`, não em
|
|
286
|
+
// `projectDir`: quando a pasta do projeto nasceu no meio do caminho, a memória
|
|
287
|
+
// precisa ir para ela, não para a raiz onde a sessão começou.
|
|
288
|
+
if (!subagent && projectName && temPasta) {
|
|
289
|
+
cfg.appendProjectMemory(pastaAtual, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
|
|
269
290
|
}
|
|
270
291
|
|
|
271
292
|
return {
|
package/lib/browser.js
CHANGED
|
@@ -36,12 +36,16 @@ function findChrome() {
|
|
|
36
36
|
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
37
37
|
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
38
38
|
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
39
|
+
'/Applications/Arc.app/Contents/MacOS/Arc',
|
|
40
|
+
'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
|
|
41
|
+
'/Applications/Opera.app/Contents/MacOS/Opera',
|
|
39
42
|
] : platform === 'win32' ? [
|
|
40
43
|
`${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
41
44
|
`${process.env['PROGRAMFILES(X86)']}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
42
45
|
`${process.env['LOCALAPPDATA']}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
43
46
|
`${process.env['PROGRAMFILES(X86)']}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
44
47
|
`${process.env['PROGRAMFILES']}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
48
|
+
`${process.env['LOCALAPPDATA']}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
|
|
45
49
|
] : [
|
|
46
50
|
'/usr/bin/google-chrome',
|
|
47
51
|
'/usr/bin/google-chrome-stable',
|
|
@@ -53,6 +57,71 @@ function findChrome() {
|
|
|
53
57
|
for (const p of candidates) {
|
|
54
58
|
try { if (p && fs.existsSync(p)) return p; } catch {}
|
|
55
59
|
}
|
|
60
|
+
|
|
61
|
+
// Último recurso: o navegador pode estar num disco montado ou numa pasta de
|
|
62
|
+
// aplicativos do usuário. Caso real medido nesta máquina: o Chrome 151 estava
|
|
63
|
+
// em `/Volumes/Google Chrome/Google Chrome.app` — a JANELA DO INSTALADOR,
|
|
64
|
+
// ainda montada. O usuário jurava (com razão) que tinha Chrome; a lista de
|
|
65
|
+
// caminhos padrão jurava que não. Os dois estavam certos.
|
|
66
|
+
const achado = procurarEmVolumes();
|
|
67
|
+
if (achado) return achado;
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Nomes de app e o binário dentro de cada um. Só Chromium: o Safari não fala o
|
|
73
|
+
// protocolo de depuração que permite controlar a página.
|
|
74
|
+
const APPS_MAC = [
|
|
75
|
+
['Google Chrome.app', 'Google Chrome'],
|
|
76
|
+
['Google Chrome Beta.app', 'Google Chrome Beta'],
|
|
77
|
+
['Google Chrome Canary.app', 'Google Chrome Canary'],
|
|
78
|
+
['Chromium.app', 'Chromium'],
|
|
79
|
+
['Microsoft Edge.app', 'Microsoft Edge'],
|
|
80
|
+
['Brave Browser.app', 'Brave Browser'],
|
|
81
|
+
['Arc.app', 'Arc'],
|
|
82
|
+
['Vivaldi.app', 'Vivaldi'],
|
|
83
|
+
['Opera.app', 'Opera'],
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Procura o navegador em `~/Applications` e nos discos montados em `/Volumes`.
|
|
88
|
+
*
|
|
89
|
+
* Um app rodando de dentro do `.dmg` funciona, mas é frágil: o disco é
|
|
90
|
+
* somente-leitura, o macOS o executa por AppTranslocation (caminho temporário
|
|
91
|
+
* aleatório) e ele desaparece na hora que o disco for ejetado. Achamos e usamos —
|
|
92
|
+
* e `avisoDeLocal()` diz ao usuário para arrastar o app para Aplicativos.
|
|
93
|
+
*/
|
|
94
|
+
function procurarEmVolumes() {
|
|
95
|
+
const bases = [];
|
|
96
|
+
try { bases.push(path.join(os.homedir(), 'Applications')); } catch {}
|
|
97
|
+
try {
|
|
98
|
+
for (const vol of fs.readdirSync('/Volumes')) bases.push(path.join('/Volumes', vol));
|
|
99
|
+
} catch {}
|
|
100
|
+
|
|
101
|
+
for (const base of bases) {
|
|
102
|
+
for (const [app, bin] of APPS_MAC) {
|
|
103
|
+
const alvo = path.join(base, app, 'Contents', 'MacOS', bin);
|
|
104
|
+
try { if (fs.existsSync(alvo)) return alvo; } catch {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* O navegador achado está num lugar problemático? Devolve o aviso, ou null.
|
|
112
|
+
*
|
|
113
|
+
* Não bloqueia nada: o controle funciona de dentro do disco montado. Só avisa,
|
|
114
|
+
* porque quem ejetar o disco vai ver a ferramenta parar de funcionar sem entender
|
|
115
|
+
* o motivo.
|
|
116
|
+
*/
|
|
117
|
+
function avisoDeLocal(caminho) {
|
|
118
|
+
if (!caminho) return null;
|
|
119
|
+
if (caminho.startsWith('/Volumes/')) {
|
|
120
|
+
const app = (caminho.match(/\/Volumes\/[^/]+\/([^/]+\.app)/) || [, 'o navegador'])[1];
|
|
121
|
+
return `${app} está rodando do disco de instalação, não instalado. `
|
|
122
|
+
+ 'Funciona, mas para de funcionar quando você ejetar esse disco — '
|
|
123
|
+
+ 'arraste o app para a pasta Aplicativos.';
|
|
124
|
+
}
|
|
56
125
|
return null;
|
|
57
126
|
}
|
|
58
127
|
|
|
@@ -100,11 +169,25 @@ class MiniWebSocket {
|
|
|
100
169
|
this.onClose = () => {};
|
|
101
170
|
this.closed = false;
|
|
102
171
|
}
|
|
103
|
-
connect(url) {
|
|
172
|
+
connect(url, timeoutMs = 10000) {
|
|
104
173
|
return new Promise((resolve, reject) => {
|
|
174
|
+
let pronto = false;
|
|
175
|
+
const relogio = setTimeout(() => {
|
|
176
|
+
if (pronto) return;
|
|
177
|
+
pronto = true;
|
|
178
|
+
try { if (this.socket) this.socket.destroy(); } catch {}
|
|
179
|
+
reject(new Error(`o navegador aceitou a conexão mas não completou o aperto de mão em ${Math.round(timeoutMs / 1000)}s`));
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
const terminar = (err) => {
|
|
182
|
+
if (pronto) return;
|
|
183
|
+
pronto = true;
|
|
184
|
+
clearTimeout(relogio);
|
|
185
|
+
err ? reject(err) : resolve();
|
|
186
|
+
};
|
|
187
|
+
|
|
105
188
|
const u = new URL(url);
|
|
106
189
|
const isSecure = u.protocol === 'wss:';
|
|
107
|
-
if (isSecure) return
|
|
190
|
+
if (isSecure) return terminar(new Error('wss:// não suportado (CDP local usa ws://)'));
|
|
108
191
|
const key = crypto.randomBytes(16).toString('base64');
|
|
109
192
|
const port = u.port || 80;
|
|
110
193
|
const path = u.pathname + (u.search || '');
|
|
@@ -126,21 +209,26 @@ class MiniWebSocket {
|
|
|
126
209
|
const sep = headerBuf.indexOf('\r\n\r\n');
|
|
127
210
|
if (sep === -1) return;
|
|
128
211
|
const header = headerBuf.slice(0, sep).toString();
|
|
129
|
-
if (!/HTTP\/1\.1 101/.test(header)) return
|
|
212
|
+
if (!/HTTP\/1\.1 101/.test(header)) return terminar(new Error('handshake WS falhou: ' + header.split('\r\n')[0]));
|
|
130
213
|
handshaked = true;
|
|
131
214
|
this.socket = sock;
|
|
132
215
|
const rest = headerBuf.slice(sep + 4);
|
|
133
216
|
if (rest.length) this._onData(rest);
|
|
134
|
-
|
|
217
|
+
terminar();
|
|
135
218
|
} else {
|
|
136
219
|
this._onData(data);
|
|
137
220
|
}
|
|
138
221
|
});
|
|
139
222
|
sock.on('error', (err) => {
|
|
140
|
-
if (!handshaked)
|
|
223
|
+
if (!handshaked) terminar(err);
|
|
141
224
|
this.closed = true; this.onClose(err);
|
|
142
225
|
});
|
|
143
|
-
sock.on('close', () => {
|
|
226
|
+
sock.on('close', () => {
|
|
227
|
+
// Fechar ANTES do 101 também é falha: sem isto a Promise ficava
|
|
228
|
+
// pendente quando o Chrome derrubava a conexão calado.
|
|
229
|
+
if (!handshaked) terminar(new Error('o navegador fechou a conexão antes de aceitar o controle'));
|
|
230
|
+
this.closed = true; this.onClose();
|
|
231
|
+
});
|
|
144
232
|
});
|
|
145
233
|
}
|
|
146
234
|
_onData(chunk) {
|
|
@@ -271,7 +359,7 @@ async function ensureChromeRunning() {
|
|
|
271
359
|
} catch {}
|
|
272
360
|
|
|
273
361
|
const chrome = findChrome();
|
|
274
|
-
if (!chrome) throw new Error('
|
|
362
|
+
if (!chrome) throw new Error('Nenhum navegador controlável encontrado. O PrimoCode usa Chrome, Edge, Brave, Arc, Vivaldi ou Chromium — instale qualquer um deles, ou aponte o seu com PRIMOCODE_CHROME=/caminho/do/navegador. (O Safari não serve: não fala o protocolo que permite controlar a página.)');
|
|
275
363
|
|
|
276
364
|
fs.mkdirSync(PROFILE_DIR, { recursive: true });
|
|
277
365
|
|
|
@@ -318,14 +406,22 @@ async function findOrCreatePageTarget(url) {
|
|
|
318
406
|
return created;
|
|
319
407
|
}
|
|
320
408
|
|
|
409
|
+
// Nenhuma etapa do controle de navegador pode pendurar o CLI sem explicação.
|
|
410
|
+
function comTeto(promessa, ms, oQue) {
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
const t = setTimeout(() => reject(new Error(`${oQue} não respondeu em ${Math.round(ms / 1000)}s`)), ms);
|
|
413
|
+
promessa.then((v) => { clearTimeout(t); resolve(v); }, (e) => { clearTimeout(t); reject(e); });
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
321
417
|
async function connect(url) {
|
|
322
418
|
await ensureChromeRunning();
|
|
323
|
-
const target = await findOrCreatePageTarget(url);
|
|
419
|
+
const target = await comTeto(findOrCreatePageTarget(url), 15000, 'a lista de abas do navegador');
|
|
324
420
|
if (!target || !target.webSocketDebuggerUrl) throw new Error('Não foi possível obter alvo de página do Chrome.');
|
|
325
421
|
if (currentSession) { try { currentSession.close(); } catch {} }
|
|
326
422
|
currentSession = new CDPSession();
|
|
327
423
|
currentSession.targetId = target.id;
|
|
328
|
-
await currentSession.attach(target.webSocketDebuggerUrl);
|
|
424
|
+
await comTeto(currentSession.attach(target.webSocketDebuggerUrl), 12000, 'o canal de controle do navegador');
|
|
329
425
|
await currentSession.send('Page.enable');
|
|
330
426
|
await currentSession.send('DOM.enable');
|
|
331
427
|
await currentSession.send('Runtime.enable');
|
|
@@ -399,6 +495,74 @@ async function clickText(text) {
|
|
|
399
495
|
return { ok: true, ...found };
|
|
400
496
|
}
|
|
401
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Espera o seletor aparecer por um instante antes de desistir.
|
|
500
|
+
*
|
|
501
|
+
* A página quase sempre ainda está montando quando a próxima ferramenta roda.
|
|
502
|
+
* Sem esta espera curta, "não encontrado" era muitas vezes "ainda não nasceu".
|
|
503
|
+
*/
|
|
504
|
+
async function esperarSeletor(selector, ms = 4000) {
|
|
505
|
+
const fim = Date.now() + ms;
|
|
506
|
+
while (Date.now() < fim) {
|
|
507
|
+
if (await evaluate(`!!document.querySelector(${JSON.stringify(selector)})`)) return true;
|
|
508
|
+
await sleep(200);
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* O que EXISTE na página, para o erro ensinar em vez de só reclamar.
|
|
515
|
+
*
|
|
516
|
+
* Mesma ideia do `trechosParecidos` do edit_file: erro seco faz o modelo chutar de
|
|
517
|
+
* novo. Devolvendo o que está lá, ele acerta na segunda tentativa.
|
|
518
|
+
*
|
|
519
|
+
* Caso real: o modelo pediu `input[name="q"]` no Google. A caixa de busca do
|
|
520
|
+
* Google é `textarea[name="q"]` — o chute era razoável e estava errado, e o erro
|
|
521
|
+
* antigo não dava nenhuma pista de qual era o certo.
|
|
522
|
+
*/
|
|
523
|
+
async function candidatosNaPagina() {
|
|
524
|
+
const js = `(function(){
|
|
525
|
+
// Ordem importa: quem errou um seletor quase sempre queria um CAMPO, e a
|
|
526
|
+
// lista é cortada em 12. Link de navegação por último, senão o campo de
|
|
527
|
+
// busca fica de fora justamente no caso em que ele é a resposta.
|
|
528
|
+
const porPrioridade = [
|
|
529
|
+
'input:not([type=hidden]),textarea,[contenteditable],[role=textbox]',
|
|
530
|
+
'button,[role=button],select,input[type=submit]',
|
|
531
|
+
'a[href]',
|
|
532
|
+
];
|
|
533
|
+
const alvos = [];
|
|
534
|
+
for (const grupo of porPrioridade) {
|
|
535
|
+
for (const e of document.querySelectorAll(grupo)) if (!alvos.includes(e)) alvos.push(e);
|
|
536
|
+
}
|
|
537
|
+
const vistos = [];
|
|
538
|
+
for (const e of alvos) {
|
|
539
|
+
const r = e.getBoundingClientRect();
|
|
540
|
+
if (r.width < 2 || r.height < 2) continue; // invisível não serve
|
|
541
|
+
const tag = e.tagName.toLowerCase();
|
|
542
|
+
let sel = tag;
|
|
543
|
+
if (e.id) sel = tag + '#' + e.id;
|
|
544
|
+
else if (e.name) sel = tag + '[name="' + e.name + '"]';
|
|
545
|
+
else if (e.getAttribute('aria-label')) sel = tag + '[aria-label="' + e.getAttribute('aria-label') + '"]';
|
|
546
|
+
else if (e.type) sel = tag + '[type="' + e.type + '"]';
|
|
547
|
+
const rotulo = (e.getAttribute('aria-label') || e.placeholder || (e.innerText||'').trim().slice(0,30) || '').replace(/\\s+/g,' ');
|
|
548
|
+
vistos.push(rotulo ? sel + ' (' + rotulo + ')' : sel);
|
|
549
|
+
if (vistos.length >= 12) break;
|
|
550
|
+
}
|
|
551
|
+
return JSON.stringify(vistos);
|
|
552
|
+
})()`;
|
|
553
|
+
try { return JSON.parse(await evaluate(js)) || []; } catch { return []; }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function erroDeSeletor(selector) {
|
|
557
|
+
const candidatos = await candidatosNaPagina();
|
|
558
|
+
const err = new Error(
|
|
559
|
+
`seletor não encontrado: ${selector}`
|
|
560
|
+
+ (candidatos.length ? `. Existem nesta página: ${candidatos.join(' · ')}` : '. A página não tem nenhum elemento clicável visível — ela pode não ter carregado.')
|
|
561
|
+
);
|
|
562
|
+
err.candidatos = candidatos;
|
|
563
|
+
return err;
|
|
564
|
+
}
|
|
565
|
+
|
|
402
566
|
async function clickSelector(selector) {
|
|
403
567
|
await ensure();
|
|
404
568
|
const js = `(function(){
|
|
@@ -408,8 +572,9 @@ async function clickSelector(selector) {
|
|
|
408
572
|
const r = el.getBoundingClientRect();
|
|
409
573
|
return { x: r.left + r.width/2, y: r.top + r.height/2, tag: el.tagName };
|
|
410
574
|
})()`;
|
|
411
|
-
|
|
412
|
-
if (!found
|
|
575
|
+
let found = await evaluate(js);
|
|
576
|
+
if (!found && await esperarSeletor(selector)) found = await evaluate(js);
|
|
577
|
+
if (!found) throw await erroDeSeletor(selector);
|
|
413
578
|
await sleep(200);
|
|
414
579
|
await click(found.x, found.y);
|
|
415
580
|
return { ok: true, selector, ...found };
|
|
@@ -426,14 +591,16 @@ async function type(text) {
|
|
|
426
591
|
|
|
427
592
|
async function typeInSelector(selector, text, clear = true) {
|
|
428
593
|
await ensure();
|
|
429
|
-
const
|
|
594
|
+
const focar = () => evaluate(`(function(){
|
|
430
595
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
431
596
|
if (!el) return false;
|
|
432
597
|
el.focus();
|
|
433
598
|
${clear ? "if ('value' in el) el.value = '';" : ''}
|
|
434
599
|
return true;
|
|
435
600
|
})()`);
|
|
436
|
-
|
|
601
|
+
let focused = await focar();
|
|
602
|
+
if (!focused && await esperarSeletor(selector)) focused = await focar();
|
|
603
|
+
if (!focused) throw await erroDeSeletor(selector);
|
|
437
604
|
await type(text);
|
|
438
605
|
// dispara evento input para frameworks reativos
|
|
439
606
|
await evaluate(`(function(){
|
|
@@ -504,5 +671,6 @@ function close() {
|
|
|
504
671
|
module.exports = {
|
|
505
672
|
open, navigate, click, clickText, clickSelector, type, typeInSelector,
|
|
506
673
|
key, screenshot, evaluate, currentUrl, waitFor, wait, scroll, close,
|
|
507
|
-
|
|
674
|
+
avisoDeLocal,
|
|
675
|
+
_internal: { findChrome, procurarEmVolumes, PROFILE_DIR, DEBUG_PORT },
|
|
508
676
|
};
|
package/lib/catalogo.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O que o PrimoCode sabe fazer — a lista, em um lugar só.
|
|
3
|
+
*
|
|
4
|
+
* Existe porque "o que ele faz" estava espalhado entre o README, o /help, o
|
|
5
|
+
* prompt do servidor e o switch do `tools.js`, e quatro cópias da mesma verdade
|
|
6
|
+
* significam que pelo menos uma está mentindo.
|
|
7
|
+
*
|
|
8
|
+
* O `test/catalogo.test.js` cruza esta lista com as tools que o `tools.execute`
|
|
9
|
+
* realmente atende: item aqui que não existe lá derruba o teste, e tool que
|
|
10
|
+
* existe lá e não está aqui também. A lista não consegue mentir.
|
|
11
|
+
*
|
|
12
|
+
* `escreve: true` marca a ferramenta que cria ou muda arquivo. É o que decide
|
|
13
|
+
* quando nasce uma pasta de projeto — pedir para abrir o Chrome não cria projeto.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const GRUPOS = [
|
|
19
|
+
{
|
|
20
|
+
nome: 'Arquivos',
|
|
21
|
+
resumo: 'ler, criar e mudar arquivo na pasta de trabalho',
|
|
22
|
+
itens: [
|
|
23
|
+
{ tool: 'write_file', escreve: true, faz: 'cria arquivo, ou reescreve inteiro' },
|
|
24
|
+
{ tool: 'edit_file', escreve: true, faz: 'troca um trecho exato, sem reescrever o resto' },
|
|
25
|
+
{ tool: 'multi_edit', escreve: true, faz: 'várias trocas no mesmo arquivo, tudo ou nada' },
|
|
26
|
+
{ tool: 'read_file', escreve: false, faz: 'lê arquivo, inteiro ou por pedaço' },
|
|
27
|
+
{ tool: 'list_dir', escreve: false, faz: 'mostra a árvore de pastas' },
|
|
28
|
+
{ tool: 'glob', escreve: false, faz: 'acha arquivo pelo nome (**/*.js)' },
|
|
29
|
+
{ tool: 'grep', escreve: false, faz: 'acha texto dentro dos arquivos' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
nome: 'Terminal',
|
|
34
|
+
resumo: 'rodar comando na sua máquina',
|
|
35
|
+
itens: [
|
|
36
|
+
{ tool: 'run_shell', escreve: false, faz: 'roda comando e devolve a saída' },
|
|
37
|
+
{ tool: 'run_in_new_terminal', escreve: false, faz: 'abre outro terminal para o que fica de pé (servidor, watch)' },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
nome: 'Navegador',
|
|
42
|
+
resumo: 'controla um navegador de verdade (Chrome, Edge, Brave, Arc…) — clica, digita, lê, fotografa',
|
|
43
|
+
itens: [
|
|
44
|
+
{ tool: 'browser_open', escreve: false, faz: 'abre uma página ou um arquivo seu' },
|
|
45
|
+
{ tool: 'browser_click', escreve: false, faz: 'clica por texto, por seletor ou por coordenada' },
|
|
46
|
+
{ tool: 'browser_type', escreve: false, faz: 'digita num campo' },
|
|
47
|
+
{ tool: 'browser_key', escreve: false, faz: 'aperta tecla (Enter, Tab, Escape)' },
|
|
48
|
+
{ tool: 'browser_read', escreve: false, faz: 'lê o texto da página para conferir o resultado' },
|
|
49
|
+
{ tool: 'browser_screenshot', escreve: true, faz: 'tira foto da página' },
|
|
50
|
+
{ tool: 'browser_scroll', escreve: false, faz: 'rola a página' },
|
|
51
|
+
{ tool: 'browser_wait', escreve: false, faz: 'espera um elemento aparecer' },
|
|
52
|
+
{ tool: 'browser_eval', escreve: false, faz: 'roda JavaScript na página' },
|
|
53
|
+
{ tool: 'open_in_browser', escreve: false, faz: 'abre no seu navegador normal, sem controlar' },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
nome: 'Computador',
|
|
58
|
+
resumo: 'mouse, teclado e tela — age em qualquer programa, não só no navegador',
|
|
59
|
+
itens: [
|
|
60
|
+
{ tool: 'desktop_screenshot', escreve: true, faz: 'tira foto da sua tela' },
|
|
61
|
+
{ tool: 'desktop_click', escreve: false, faz: 'clica na tela' },
|
|
62
|
+
{ tool: 'desktop_move', escreve: false, faz: 'move o mouse' },
|
|
63
|
+
{ tool: 'desktop_drag', escreve: false, faz: 'arrasta de um ponto a outro' },
|
|
64
|
+
{ tool: 'desktop_type', escreve: false, faz: 'digita no programa que está na frente' },
|
|
65
|
+
{ tool: 'desktop_key', escreve: false, faz: 'aperta tecla ou atalho' },
|
|
66
|
+
{ tool: 'desktop_scroll', escreve: false, faz: 'rola a tela' },
|
|
67
|
+
{ tool: 'desktop_size', escreve: false, faz: 'mede a tela, para se orientar antes de clicar' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
nome: 'Organização',
|
|
72
|
+
resumo: 'como ele se organiza numa tarefa grande',
|
|
73
|
+
itens: [
|
|
74
|
+
{ tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou' },
|
|
75
|
+
// Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
|
|
76
|
+
{ tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true },
|
|
77
|
+
{ tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/** Só as que criam ou mudam arquivo. É o gatilho da pasta de projeto. */
|
|
83
|
+
const ESCREVEM = new Set(
|
|
84
|
+
GRUPOS.flatMap((g) => g.itens.filter((i) => i.escreve).map((i) => i.tool)),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const TODAS = GRUPOS.flatMap((g) => g.itens.map((i) => i.tool));
|
|
88
|
+
|
|
89
|
+
/** Esta ferramenta cria ou muda arquivo? */
|
|
90
|
+
function escreveArquivo(tool) {
|
|
91
|
+
return ESCREVEM.has(tool);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { GRUPOS, TODAS, ESCREVEM, escreveArquivo };
|
package/lib/pasta.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Descobrir em que pasta trabalhar a partir do que a pessoa escreveu.
|
|
3
|
+
*
|
|
4
|
+
* A regra do produto: por padrão ele trabalha em `~/primocode`, sempre, não
|
|
5
|
+
* importa de onde foi aberto. Se você **falar** de uma pasta no pedido, ele
|
|
6
|
+
* procura essa pasta no seu computador e muda para ela sozinho.
|
|
7
|
+
*
|
|
8
|
+
* Duas funções, de propósito: `detectarPasta` é pura (texto → candidato) e
|
|
9
|
+
* `resolverPasta` toca o disco. Assim a parte que erra feio — decidir que uma
|
|
10
|
+
* palavra qualquer é nome de pasta — é testável sem criar arquivo nenhum.
|
|
11
|
+
*
|
|
12
|
+
* ── POR QUE CONSERVADOR ──────────────────────────────────────────────────
|
|
13
|
+
* Errar para MENOS custa um `/dir` a mais. Errar para MAIS faz o agente escrever
|
|
14
|
+
* arquivo dentro de uma pasta que ninguém escolheu. Então: só detecta quando há
|
|
15
|
+
* marca explícita ("pasta X", "~/x/y") e nunca quando o texto parece nome de
|
|
16
|
+
* arquivo.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
// Onde procurar uma pasta pelo nome, na ordem. É onde as pessoas guardam
|
|
26
|
+
// trabalho — não é varredura do disco, que seria lenta e traria lixo.
|
|
27
|
+
const ONDE_PROCURAR = [
|
|
28
|
+
'', // a própria raiz de trabalho (~/primocode)
|
|
29
|
+
'Downloads',
|
|
30
|
+
'Documents',
|
|
31
|
+
'Documentos',
|
|
32
|
+
'Desktop',
|
|
33
|
+
'Área de Trabalho',
|
|
34
|
+
'projetos',
|
|
35
|
+
'Projetos',
|
|
36
|
+
'dev',
|
|
37
|
+
'code',
|
|
38
|
+
'repos',
|
|
39
|
+
'git',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Palavras que vêm depois de "pasta" mas não são nome de pasta nenhuma.
|
|
43
|
+
const NAO_E_NOME = new Set([
|
|
44
|
+
'do', 'da', 'de', 'dos', 'das', 'no', 'na', 'em', 'o', 'a', 'os', 'as',
|
|
45
|
+
'projeto', 'atual', 'raiz', 'aqui', 'certa', 'mesma', 'essa', 'esta',
|
|
46
|
+
'dele', 'minha', 'meu', 'nova', 'novo', 'onde', 'que', 'qual', 'toda',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Extensão de arquivo: "index.html" nunca é pasta. Sem isto, "crie o
|
|
50
|
+
// src/index.html" viraria uma troca de pasta para "src/index.html".
|
|
51
|
+
const TEM_EXTENSAO = /\.[a-z0-9]{1,5}$/i;
|
|
52
|
+
|
|
53
|
+
const CARACTERES_DE_CAMINHO = 'A-Za-z0-9À-ÖØ-öø-ÿ_~./\\\\-';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* O texto menciona uma pasta? Devolve o candidato cru, ou null.
|
|
57
|
+
*
|
|
58
|
+
* Aceita, em ordem de confiança:
|
|
59
|
+
* 1. caminho explícito: `~/projetos/loja`, `/Users/joel/x`
|
|
60
|
+
* 2. marca de pasta: `na pasta loja`, `diretório src/app`
|
|
61
|
+
* 3. caminho relativo com barra e sem extensão: `Downloads/meu-site`
|
|
62
|
+
*/
|
|
63
|
+
function detectarPasta(texto) {
|
|
64
|
+
const s = String(texto || '');
|
|
65
|
+
if (!s.trim()) return null;
|
|
66
|
+
|
|
67
|
+
// 1. Caminho explícito — `~/...` ou absoluto com pelo menos dois segmentos.
|
|
68
|
+
// Um `/algo` solto não conta: seria confundido com comando (`/dir`).
|
|
69
|
+
const explicito = s.match(new RegExp(`(?:^|\\s)(~[${CARACTERES_DE_CAMINHO}]*|/[${CARACTERES_DE_CAMINHO}]+/[${CARACTERES_DE_CAMINHO}]*)`));
|
|
70
|
+
if (explicito) {
|
|
71
|
+
const bruto = limpar(explicito[1]);
|
|
72
|
+
if (bruto && !TEM_EXTENSAO.test(bruto)) return bruto;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Marca explícita de pasta.
|
|
76
|
+
const marcado = s.match(new RegExp(`\\b(?:pasta|diret[óo]rio|dir)\\s+(?:d[eoa]s?\\s+|no\\s+|na\\s+|em\\s+)?["'\`]?([${CARACTERES_DE_CAMINHO}]+)["'\`]?`, 'i'));
|
|
77
|
+
if (marcado) {
|
|
78
|
+
const bruto = limpar(marcado[1]);
|
|
79
|
+
if (bruto && !TEM_EXTENSAO.test(bruto) && !NAO_E_NOME.has(bruto.toLowerCase())) return bruto;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. Caminho relativo com barra e sem extensão.
|
|
83
|
+
// O ponto ENTRA no token de propósito: sem ele, `src/app.js` casaria como
|
|
84
|
+
// `src/app` e passaria pelo filtro de extensão sem ser visto. Capturando a
|
|
85
|
+
// extensão junto, o TEM_EXTENSAO abaixo consegue recusar.
|
|
86
|
+
const relativo = s.match(new RegExp(`(?:^|\\s)([A-Za-zÀ-ÖØ-öø-ÿ0-9_.-]+(?:/[A-Za-zÀ-ÖØ-öø-ÿ0-9_.-]+)+)`));
|
|
87
|
+
if (relativo) {
|
|
88
|
+
const bruto = limpar(relativo[1]);
|
|
89
|
+
if (bruto && !TEM_EXTENSAO.test(bruto)) return bruto;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Tira pontuação de fim de frase e barra final — "na pasta loja." → "loja".
|
|
96
|
+
function limpar(bruto) {
|
|
97
|
+
return String(bruto || '').trim().replace(/[.,;:!?)\]}]+$/, '').replace(/\/+$/, '');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Transforma o candidato em pasta de verdade no disco.
|
|
102
|
+
*
|
|
103
|
+
* Devolve `{ caminho, ondeAchou }` quando há uma só, `{ ambiguo: [...] }` quando
|
|
104
|
+
* há mais de uma (aí quem decide é o usuário, não a heurística), e `null` quando
|
|
105
|
+
* não achou nada.
|
|
106
|
+
*/
|
|
107
|
+
function resolverPasta(candidato, opts = {}) {
|
|
108
|
+
const bruto = String(candidato || '').trim();
|
|
109
|
+
if (!bruto) return null;
|
|
110
|
+
|
|
111
|
+
const home = opts.home || os.homedir();
|
|
112
|
+
const raiz = opts.raiz || path.join(home, 'primocode');
|
|
113
|
+
const cwd = opts.cwd || process.cwd();
|
|
114
|
+
|
|
115
|
+
// Caminho que já se explica: `~/x`, `/Users/...`, `./x`, `../x`.
|
|
116
|
+
if (/^~(?:$|[/\\])/.test(bruto) || path.isAbsolute(bruto) || /^\.\.?[/\\]/.test(bruto)) {
|
|
117
|
+
const alvo = path.resolve(cwd, bruto.replace(/^~(?=$|[/\\])/, home));
|
|
118
|
+
return ehPasta(alvo) ? { caminho: alvo, ondeAchou: 'caminho' } : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Nome ou caminho relativo: procura nos lugares prováveis, na ordem.
|
|
122
|
+
const achados = [];
|
|
123
|
+
const bases = [raiz, cwd, home, ...ONDE_PROCURAR.map((p) => path.join(home, p))];
|
|
124
|
+
for (const base of bases) {
|
|
125
|
+
if (!base) continue;
|
|
126
|
+
const alvo = path.join(base, bruto);
|
|
127
|
+
if (!ehPasta(alvo)) continue;
|
|
128
|
+
const real = canonico(alvo);
|
|
129
|
+
if (achados.some((a) => canonico(a.caminho) === real)) continue;
|
|
130
|
+
achados.push({ caminho: alvo, ondeAchou: base === raiz ? 'primocode' : base });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Ainda nada? Um nível dentro de cada lugar provável — é onde mora
|
|
134
|
+
// `~/Downloads/clientes/loja-do-joao`.
|
|
135
|
+
if (!achados.length) {
|
|
136
|
+
for (const base of bases) {
|
|
137
|
+
for (const filho of listarPastas(base)) {
|
|
138
|
+
const alvo = path.join(base, filho, bruto);
|
|
139
|
+
if (!ehPasta(alvo)) continue;
|
|
140
|
+
const real = canonico(alvo);
|
|
141
|
+
if (achados.some((a) => canonico(a.caminho) === real)) continue;
|
|
142
|
+
achados.push({ caminho: alvo, ondeAchou: path.join(base, filho) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!achados.length) return null;
|
|
148
|
+
if (achados.length === 1) return achados[0];
|
|
149
|
+
return { ambiguo: achados.slice(0, 5) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function ehPasta(p) {
|
|
153
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Identidade da pasta no disco — `dispositivo:inode`.
|
|
158
|
+
*
|
|
159
|
+
* Necessário porque no macOS e no Windows o sistema de arquivos não diferencia
|
|
160
|
+
* maiúscula: `~/projetos` e `~/Projetos` são a MESMA pasta, e a lista de lugares
|
|
161
|
+
* prováveis tem as duas grafias. Sem isto, procurar "duplicado" achava a mesma
|
|
162
|
+
* pasta duas vezes e virava "ambíguo" sem haver ambiguidade nenhuma.
|
|
163
|
+
*
|
|
164
|
+
* Comparar `realpathSync` não resolve: no macOS ele resolve symlink mas **não**
|
|
165
|
+
* corrige a caixa — medido aqui, `.../Projetos/duplicado` volta com o P maiúsculo.
|
|
166
|
+
* O inode volta igual (850551 nos dois), então é ele que diz a verdade. Também
|
|
167
|
+
* pega symlink e hard link de graça, o que é o comportamento certo.
|
|
168
|
+
*/
|
|
169
|
+
function canonico(p) {
|
|
170
|
+
try {
|
|
171
|
+
const st = fs.statSync(p);
|
|
172
|
+
return `${st.dev}:${st.ino}`;
|
|
173
|
+
} catch {
|
|
174
|
+
return path.resolve(p);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function listarPastas(base) {
|
|
179
|
+
try {
|
|
180
|
+
return fs.readdirSync(base, { withFileTypes: true })
|
|
181
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
182
|
+
.map((e) => e.name)
|
|
183
|
+
.slice(0, 40);
|
|
184
|
+
} catch { return []; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { detectarPasta, resolverPasta, ONDE_PROCURAR };
|
package/lib/tools.js
CHANGED
|
@@ -317,11 +317,41 @@ function writeTodos({ todos }) {
|
|
|
317
317
|
}
|
|
318
318
|
function getTodos() { return TODOS; }
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Executa uma ferramenta e NUNCA lança.
|
|
322
|
+
*
|
|
323
|
+
* O `await` aqui não é enfeite. O despachante abaixo faz `return browser.x(...)`
|
|
324
|
+
* em vinte casos, e `try { return promessa } catch` **não** pega a rejeição dessa
|
|
325
|
+
* promessa — só `return await` pega. Sem isto, qualquer falha de navegador ou
|
|
326
|
+
* desktop subia por cima do loop do agente e derrubava o CLI inteiro com exit 1,
|
|
327
|
+
* em vez de virar uma ação falhada que o modelo pode corrigir.
|
|
328
|
+
*
|
|
329
|
+
* Foi exatamente o que acontecia ao pedir uma pesquisa no Google: o seletor não
|
|
330
|
+
* batia, a promessa rejeitava, e o programa morria calado no meio da tarefa.
|
|
331
|
+
*/
|
|
320
332
|
async function execute(name, args, ctx) {
|
|
333
|
+
try {
|
|
334
|
+
return await despachar(name, args, ctx);
|
|
335
|
+
} catch (e) {
|
|
336
|
+
// Erro que sabe o que EXISTE leva a informação junto — mesma ideia do
|
|
337
|
+
// `trechosParecidos` do edit_file. Erro seco faz o modelo chutar de novo.
|
|
338
|
+
if (e && Array.isArray(e.candidatos) && e.candidatos.length) {
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
error: e.message,
|
|
342
|
+
seletoresDisponiveis: e.candidatos,
|
|
343
|
+
comoCorrigir: 'Chame a tool de novo com um seletor de "seletoresDisponiveis", copiado LITERALMENTE. O primeiro da lista é o mais provável.',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return { ok: false, error: e.message || String(e) };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function despachar(name, args, ctx) {
|
|
321
351
|
const projectDir = (ctx && ctx.projectDir) || process.cwd();
|
|
322
352
|
args = args || {};
|
|
323
353
|
|
|
324
|
-
|
|
354
|
+
{
|
|
325
355
|
switch (name) {
|
|
326
356
|
case 'write_file': {
|
|
327
357
|
const filePath = resolveInProject(args.path, projectDir);
|
|
@@ -410,8 +440,6 @@ async function execute(name, args, ctx) {
|
|
|
410
440
|
default:
|
|
411
441
|
return { ok: false, error: `Tool não implementada: ${name}` };
|
|
412
442
|
}
|
|
413
|
-
} catch (e) {
|
|
414
|
-
return { ok: false, error: e.message };
|
|
415
443
|
}
|
|
416
444
|
}
|
|
417
445
|
|
package/lib/ui.js
CHANGED
|
@@ -161,9 +161,9 @@ const TIPS = [
|
|
|
161
161
|
];
|
|
162
162
|
|
|
163
163
|
const NEWS = [
|
|
164
|
-
'
|
|
165
|
-
'
|
|
166
|
-
'
|
|
164
|
+
'Modelos grátis, com reserva: um no limite não vira erro na sua tela',
|
|
165
|
+
'Ele trabalha em ~/primocode. Fale o nome de uma pasta e ele muda sozinho',
|
|
166
|
+
'Falha de navegador agora diz quais seletores EXISTEM na página',
|
|
167
167
|
];
|
|
168
168
|
|
|
169
169
|
// ── Cabeçalho ────────────────────────────────────────────────────────────
|
|
@@ -172,7 +172,7 @@ const NEWS = [
|
|
|
172
172
|
// alinhamento do desenho já dá a borda esquerda de graça.
|
|
173
173
|
|
|
174
174
|
function welcomeBox(version, contexto, opts = {}) {
|
|
175
|
-
const { mascot: style, userName, modo } = opts;
|
|
175
|
+
const { mascot: style, userName, modo, pastaPadrao } = opts;
|
|
176
176
|
const art = mascot(style);
|
|
177
177
|
const cols = Math.max(32, Math.min(process.stdout.columns || 100, 120));
|
|
178
178
|
|
|
@@ -182,10 +182,20 @@ function welcomeBox(version, contexto, opts = {}) {
|
|
|
182
182
|
|
|
183
183
|
// As três linhas de identidade — uma por linha do mascote, na ordem em que
|
|
184
184
|
// se lê: quem é, quem chegou, onde estamos.
|
|
185
|
+
// A pasta é o que mais confundia quem usa, então ela avisa de si mesma — na
|
|
186
|
+
// MESMA linha de sempre, sem mudar o desenho do cabeçalho. Amber quando é a
|
|
187
|
+
// pasta padrão (é um aviso, não uma falha: dá para trabalhar em outra), e a
|
|
188
|
+
// primária quando foi você que escolheu — aí não há nada a avisar.
|
|
189
|
+
const linhaPasta = !contexto
|
|
190
|
+
? c.dim('nenhuma pasta ativa')
|
|
191
|
+
: pastaPadrao
|
|
192
|
+
? `${c.warn(contexto)}${c.dim(' · /dir <pasta> muda')}`
|
|
193
|
+
: c.brandDim(contexto);
|
|
194
|
+
|
|
185
195
|
const ident = [
|
|
186
196
|
`${c.bold(c.brand(NOME))} ${c.dim('v' + version)}`,
|
|
187
197
|
saudacao,
|
|
188
|
-
|
|
198
|
+
linhaPasta,
|
|
189
199
|
];
|
|
190
200
|
|
|
191
201
|
const out = [''];
|
|
@@ -331,6 +341,15 @@ function friendlyError(raw) {
|
|
|
331
341
|
+ '. Adicione crédito em openrouter.ai/settings/credits, ou use /model fast'
|
|
332
342
|
+ ' para pedidos menores.';
|
|
333
343
|
}
|
|
344
|
+
// O corte de prompt da conta sem crédito. Mensagem própria porque a cura é
|
|
345
|
+
// outra: não é encurtar a conversa nem esperar, é trocar de modelo — e os
|
|
346
|
+
// apelidos padrão já são grátis, então quem cai aqui forçou um modelo pago.
|
|
347
|
+
if (/prompt tokens limit exceeded/i.test(low)) {
|
|
348
|
+
const n = s.match(/exceeded:\s*(\d+)\s*>\s*(\d+)/i);
|
|
349
|
+
return '🆓 Este modelo é pago e a conta não tem crédito, então o provedor corta o pedido'
|
|
350
|
+
+ (n ? ` em ${n[2]} tokens (o seu tem ${n[1]})` : '')
|
|
351
|
+
+ '. Use /model main — os modelos padrão são grátis e não têm esse corte.';
|
|
352
|
+
}
|
|
334
353
|
// Ordem importa: "grande demais" e "muitos pedidos" chegam os dois como
|
|
335
354
|
// rate_limit_exceeded, e a saída é oposta. Tamanho não se resolve
|
|
336
355
|
// esperando — o pedido vai estourar igual daqui a uma hora.
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
4
|
-
"description": "PrimoCode — agente de engenharia com IA e cursor próprio.
|
|
3
|
+
"version": "7.9.0",
|
|
4
|
+
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"primocode": "bin/primocode.js"
|
|
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"
|
|
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"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|