primocode 7.7.0 → 7.8.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 CHANGED
@@ -1,13 +1,17 @@
1
- # PrimoCode v7.7.0
1
+ # PrimoCode v7.8.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.7.0
12
+ ▐▛███▜▌ Primo Code v7.8.0
9
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
10
- ▘▘ ▝▝ ~/projetos/jogo-de-carro
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
- | Abriu o PrimoCode dentro de uma pasta | é ali que ele trabalha |
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 = Opus 5, main = Sonnet 5 (padrão), fast = Haiku 4.5. Aceita qualquer ID do OpenRouter |
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, // 'cwd' | '/dir' | 'projeto' — para o /dir explicar
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('Abriu dentro de uma pasta? É ali. Senão, /dir aponta para qualquer')}
65
- ${c.muted('pasta sua, e /project cria uma nova em ~/primocode.')}
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
- // Como o Claude Code: se você abriu o PrimoCode dentro de uma pasta de
124
- // trabalho, é ali que ele trabalha. Home e raiz de disco não contam — ninguém
125
- // quer um agente soltando arquivo no meio de C:\Users\você.
126
- function isUsableCwd(dir) {
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. A pasta só é criada de verdade em
136
- // createPendingProject(), quando o agente for mexer em arquivo.
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.workDir) return { dir: state.workDir, pending: false };
139
- const proposed = deriveProjectName(userText);
140
- const similar = cfg.findSimilarProject(proposed);
141
- const name = similar ? similar.name : proposed;
142
- return { dir: cfg.getProjectDir(name), pending: true, name, similar: !!similar };
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 = planWorkDir(userText);
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.pending ? plan.name : state.currentProject,
190
- // Só chamado se o agente for mesmo mexer em arquivo.
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 = { cwd: 'a pasta onde você abriu o PrimoCode', '/dir': 'você escolheu com /dir', projeto: 'projeto criado pelo PrimoCode' };
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 (!isUsableCwd(alvo)) {
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: 'Opus 5 — o mais capaz, e o mais caro por pedido',
339
- main: 'Sonnet 5 — o padrão: capacidade de topo a 2,5x menos',
340
- fast: 'Haiku 4.5 — respostas curtas e baratas',
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
- // Abriu dentro de uma pasta de trabalho? Então é ali que ele atua.
489
- if (isUsableCwd(process.cwd())) {
490
- state.workDir = process.cwd();
491
- state.currentProject = path.basename(state.workDir);
492
- state.workDirOrigem = 'cwd';
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
- const plan = planWorkDir(promptText);
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.pending ? plan.name : state.currentProject,
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 mexer em arquivo. Um "oi"
140
- // não deve deixar diretório vazio espalhado pelo disco.
141
- let workDirReady = !ensureWorkDir;
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 (!workDirReady) { ensureWorkDir(); workDirReady = true; }
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.
@@ -263,9 +273,11 @@ async function runAct(userPrompt, opts) {
263
273
 
264
274
  if (!finalText) finalText = 'Cheguei ao limite de passos sem concluir. Refaça o pedido em partes menores.';
265
275
 
266
- // Nada de arquivo mexido = nada para lembrar.
267
- if (!subagent && projectName && workDirReady) {
268
- cfg.appendProjectMemory(projectDir, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
276
+ // Nada de arquivo mexido = nada para lembrar. E grava em `pastaAtual`, não em
277
+ // `projectDir`: quando a pasta do projeto nasceu no meio do caminho, a memória
278
+ // precisa ir para ela, não para a raiz onde a sessão começou.
279
+ if (!subagent && projectName && temPasta) {
280
+ cfg.appendProjectMemory(pastaAtual, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
269
281
  }
270
282
 
271
283
  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',
@@ -271,7 +275,7 @@ async function ensureChromeRunning() {
271
275
  } catch {}
272
276
 
273
277
  const chrome = findChrome();
274
- if (!chrome) throw new Error('Google Chrome não encontrado no sistema. Instale o Chrome para usar o controle de navegador.');
278
+ 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
279
 
276
280
  fs.mkdirSync(PROFILE_DIR, { recursive: true });
277
281
 
@@ -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/ui.js CHANGED
@@ -161,9 +161,9 @@ const TIPS = [
161
161
  ];
162
162
 
163
163
  const NEWS = [
164
- 'Modo auto: conversa vai pro chat, tarefa vai pro agente — gasta menos',
165
- '/dir aponta para qualquer pasta sua, sem criar projeto novo',
166
- 'Corrigido o 413 que fazia todo pedido do modo primocode falhar',
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
+ '/tudo lista as 30 ferramentas que ele sabe usar',
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
- contexto ? c.brandDim(contexto) : c.dim('nenhuma pasta ativa'),
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.7.0",
4
- "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modo primocode cria arquivos, controla o desktop e navegador. Modo chat é conversa pura.",
3
+ "version": "7.8.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"