primocode 7.5.0 → 7.7.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,11 +1,11 @@
1
- # PrimoCode v7.5.0
1
+ # PrimoCode v7.7.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
7
  ```
8
- ▐▛███▜▌ Primo Code v7.5.0
8
+ ▐▛███▜▌ Primo Code v7.7.0
9
9
  ▝▜█████▛▘ Bem-vindo de volta, Joel
10
10
  ▘▘ ▝▝ ~/projetos/jogo-de-carro
11
11
  ```
@@ -23,7 +23,7 @@ veja `/mascot` e `/name`.
23
23
  | **Memória** | Cada projeto lembra o que foi feito, entre sessões | Esquece ao fechar |
24
24
  | **Instalação** | Node puro, **zero dependências** — segundos | Pacotes e dependências |
25
25
  | **Cara própria** | Mascote, nome e temas que você escolhe | Fixo |
26
- | **Custo** | Roda em modelos rápidos e baratos (Groq/Llama) | Cobrança por token da API |
26
+ | **Modelos** | Claude Opus 5, Sonnet 5 e Haiku 4.5 via OpenRouter — trocáveis com `/model` | Preso ao provedor do fabricante |
27
27
 
28
28
  Em uma frase: **é o poder de um agente de código no terminal, em português, controlando o seu computador de verdade — sem você precisar configurar chave de API nenhuma.**
29
29
 
@@ -93,10 +93,43 @@ fim. É por isso que a IA não esquece o projeto entre sessões.
93
93
 
94
94
  | Modo | O que faz |
95
95
  |---|---|
96
- | **Primo Code** (padrão) | Agente completo — cria arquivos, roda comandos, abre o navegador |
96
+ | **Auto** (padrão) | Escolhe por pedido: conversa vai pro chat, tarefa vai pro agente |
97
+ | **Primo Code** | Agente completo — cria arquivos, roda comandos, abre o navegador |
97
98
  | **Chat** | Conversa pura, sem tools |
98
99
 
99
- Alterne com `/mode primocode` ou `/mode chat`.
100
+ Alterne com `/mode auto`, `/mode primocode` ou `/mode chat`. A escolha fica
101
+ guardada entre sessões.
102
+
103
+ **Por que o auto é o padrão.** O modo Primo Code manda o catálogo das 30
104
+ ferramentas em todo pedido — cerca de 4.700 tokens antes de você digitar a
105
+ primeira letra. Um "oi" custava o mesmo que uma refatoração, e num plano com
106
+ teto diário isso acaba a cota no meio da tarde. O auto decide na sua máquina,
107
+ sem chamar o modelo para isso, e mostra o que escolheu:
108
+
109
+ ```
110
+ Auto ❯ oi
111
+ ⎿ Chat · cumprimento
112
+
113
+ Auto·chat ❯ conserta o bug do login em src/auth.js
114
+ ⎿ Primo Code · cita arquivo ou caminho
115
+ ```
116
+
117
+ Na dúvida ele vai para o chat: errar para chat custa repetir o pedido, errar
118
+ para o agente custa token e pode escrever arquivo que ninguém pediu.
119
+
120
+ ## Onde ele trabalha
121
+
122
+ | Situação | Pasta de trabalho |
123
+ |---|---|
124
+ | Abriu o PrimoCode dentro de uma pasta | é ali que ele trabalha |
125
+ | `/dir ~/meus-projetos/site` | a pasta que **você** escolher, onde ela estiver |
126
+ | `/project loja` | cria `~/primocode/loja`, com memória própria |
127
+
128
+ `/dir` sem argumento mostra a pasta ativa e de onde ela veio. Caminho relativo
129
+ resolve contra a pasta ativa, então depois de um `/dir` o `/dir src` entra na
130
+ subpasta. Se a pasta não existir, ele pergunta antes de criar; se for a sua home
131
+ ou a raiz do disco, pergunta de novo — é onde um arquivo escrito por engano dói
132
+ mais.
100
133
 
101
134
  ## Ferramentas do agente
102
135
 
@@ -205,9 +238,10 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
205
238
 
206
239
  | Comando | Descrição |
207
240
  |---|---|
208
- | `/project` | Lista projetos. `/project <nome>` cria ou entra em um |
209
- | `/mode primocode\|chat` | Alterna o modo |
210
- | `/model main\|fast` | main = llama-3.3-70b, fast = llama-3.1-8b |
241
+ | `/dir <caminho>` | Trabalha numa pasta que já existe, onde você quiser |
242
+ | `/project` | Lista projetos. `/project <nome>` cria em `~/primocode` |
243
+ | `/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 |
211
245
  | `/attach <arquivo>` | Anexa arquivo ao próximo pedido |
212
246
  | `/detach` · `/files` | Remove / lista anexos |
213
247
  | `/tree [n]` | Árvore do projeto atual |
@@ -252,7 +286,7 @@ Caminho absoluto ou com `../` é recusado — o agente não escreve fora da sua
252
286
 
253
287
  ```
254
288
  ┌────────────────┐ HTTPS/SSE ┌───────────────────┐ HTTPS ┌──────────┐
255
- │ primocode CLI │ ─────────────► │ primocode-server │ ────────► │ Groq │
289
+ │ primocode CLI │ ─────────────► │ primocode-server │ ────────► │OpenRouter│
256
290
  │ (seu terminal)│ ◄───────────── │ (Render) │ ◄──────── │ API │
257
291
  └────────────────┘ └───────────────────┘ └──────────┘
258
292
  executa as tools monta o prompt
package/bin/primocode.js CHANGED
@@ -15,6 +15,7 @@ const os = require('os');
15
15
  const pkg = require('../package.json');
16
16
  const ui = require('../lib/ui');
17
17
  const { c, welcomeBox, Spinner } = ui;
18
+ const { escolherModo } = require('../lib/modo.js');
18
19
  const api = require('../lib/api');
19
20
  const tools = require('../lib/tools');
20
21
  const cfg = require('../lib/config');
@@ -25,18 +26,21 @@ const VERSION = pkg.version;
25
26
  const state = {
26
27
  config: cfg.loadConfig(),
27
28
  history: [],
28
- mode: 'primocode',
29
+ mode: null, // 'primocode' | 'chat' | 'auto' — vem da config em main()
30
+ modoEfetivo: null, // no auto, o modo que o último pedido usou de fato
29
31
  attachedFiles: [],
30
32
  lastResponse: '',
31
33
  currentProject: null,
32
- workDir: null, // pasta onde o agente trabalha nesta sessão
34
+ workDir: null, // pasta onde o agente trabalha nesta sessão
35
+ workDirOrigem: null, // 'cwd' | '/dir' | 'projeto' — para o /dir explicar
33
36
  };
34
37
 
35
38
  const HELP = `
36
39
  ${c.bold('Comandos')}
37
- ${c.brand('/project')} lista projetos ou muda: /project <nome>
38
- ${c.brand('/mode')} alterna: /mode primocode|chat
39
- ${c.brand('/model')} alterna: /model main|fast
40
+ ${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
41
+ ${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
42
+ ${c.brand('/mode')} /mode primocode|chat|auto
43
+ ${c.brand('/model')} /model top|main|fast — ou um ID do OpenRouter
40
44
  ${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
41
45
  ${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
42
46
  ${c.brand('/tree')} [n] árvore do projeto
@@ -52,14 +56,27 @@ ${c.bold('Comandos')}
52
56
  ${c.brand('/exit')} sair
53
57
 
54
58
  ${c.bold('Modos')}
55
- ${c.magenta('Primo Code')} ${c.muted('Agente completo — cria arquivos, roda comandos, abre o navegador')}
56
- ${c.brand('Chat')} ${c.muted('Conversa pura — sem tools, só diálogo')}
59
+ ${c.warn('Primo Code')} ${c.muted('Agente completo — cria arquivos, roda comandos, abre o navegador')}
60
+ ${c.brandDim('Chat')} ${c.muted('Conversa pura — sem tools, só diálogo. Gasta bem menos token')}
61
+ ${c.brand('Auto')} ${c.muted('Escolhe por pedido: conversa vai pro chat, tarefa vai pro agente')}
62
+
63
+ ${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.')}
57
66
  `;
58
67
 
59
68
  let rl = null;
60
69
 
61
70
  function makePrompt() {
62
- const modeTag = state.mode === 'primocode' ? c.magenta('Primo Code') : c.brand('Chat');
71
+ // No auto o rótulo mostra "Auto" e, depois do primeiro pedido, qual modo
72
+ // ele acabou usando — senão você nunca sabe quem respondeu.
73
+ let modeTag;
74
+ if (state.mode === 'auto') {
75
+ const ultimo = state.modoEfetivo === 'primocode' ? c.warn('primo') : state.modoEfetivo === 'chat' ? c.brandDim('chat') : '';
76
+ modeTag = c.brand('Auto') + (ultimo ? c.dim('·') + ultimo : '');
77
+ } else {
78
+ modeTag = state.mode === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
79
+ }
63
80
  const proj = state.currentProject ? c.dim(` ${state.currentProject}`) : '';
64
81
  const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
65
82
  return `\n${c.muted(modeTag)}${proj}${att} ${c.brand('❯')} `;
@@ -129,6 +146,7 @@ function createPendingProject(plan) {
129
146
  const dir = cfg.createProjectDir(plan.name);
130
147
  state.workDir = dir;
131
148
  state.currentProject = path.basename(dir);
149
+ state.workDirOrigem = 'projeto';
132
150
  const rotulo = plan.similar ? 'Continuando em' : 'Projeto novo:';
133
151
  console.log(c.ok(` ${rotulo} ${state.currentProject}`) + c.muted(` (${dir})`));
134
152
  refreshPrompt();
@@ -148,7 +166,19 @@ async function sendPrompt(userText) {
148
166
  prompt = parts.join('\n\n') + '\n\n' + userText;
149
167
  }
150
168
 
151
- if (state.mode === 'primocode') {
169
+ // O modo primocode manda o schema das 30 tools em todo pedido (~4.700
170
+ // tokens). No auto, quem não vai usar ferramenta não paga por ela.
171
+ let modo = state.mode;
172
+ if (modo === 'auto') {
173
+ const escolha = escolherModo(userText);
174
+ modo = escolha.modo;
175
+ state.modoEfetivo = modo;
176
+ const rotulo = modo === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
177
+ console.log(c.dim(` ⎿ ${ui.stripAnsi(rotulo)} · ${escolha.motivo}`));
178
+ refreshPrompt();
179
+ }
180
+
181
+ if (modo === 'primocode') {
152
182
  const plan = planWorkDir(userText);
153
183
  const r = await runAct(prompt, {
154
184
  server: state.config.server,
@@ -232,22 +262,93 @@ async function handleSlash(text) {
232
262
  const dir = cfg.createProjectDir(rest);
233
263
  state.workDir = dir;
234
264
  state.currentProject = path.basename(dir);
265
+ state.workDirOrigem = 'projeto';
235
266
  console.log(c.ok(`Projeto: ${state.currentProject}`) + c.muted(` (${dir})`));
236
267
  refreshPrompt();
237
268
  break;
238
269
  }
239
270
 
271
+ // Antes só existia /project, que SEMPRE cria em ~/primocode/<nome>. Se a
272
+ // sua pasta já existe em outro lugar, não havia como apontar para ela.
273
+ case 'dir':
274
+ case 'cd':
275
+ case 'pasta': {
276
+ if (!rest) {
277
+ const de = { cwd: 'a pasta onde você abriu o PrimoCode', '/dir': 'você escolheu com /dir', projeto: 'projeto criado pelo PrimoCode' };
278
+ console.log(` ${c.white(shortPath(projectDir()))}`);
279
+ console.log(c.muted(` ${de[state.workDirOrigem] || 'pasta atual do terminal'}`));
280
+ console.log(c.dim(' /dir <caminho> muda · /project <nome> cria uma nova em ~/primocode'));
281
+ break;
282
+ }
283
+
284
+ // ~ e caminho relativo resolvem contra a pasta ativa, não contra o
285
+ // cwd do processo: depois de um /dir, "src" é dentro dela.
286
+ const bruto = rest.replace(/^~(?=$|[/\\])/, os.homedir());
287
+ const alvo = path.resolve(projectDir(), bruto);
288
+
289
+ if (!fs.existsSync(alvo)) {
290
+ if (!(await confirm(`A pasta ${shortPath(alvo)} não existe. Criar?`))) {
291
+ console.log(c.muted(' Nada mudou.'));
292
+ break;
293
+ }
294
+ try { fs.mkdirSync(alvo, { recursive: true }); }
295
+ catch (e) { console.log(c.err(` Não consegui criar: ${e.message}`)); break; }
296
+ } else if (!fs.statSync(alvo).isDirectory()) {
297
+ console.log(c.err(` ${shortPath(alvo)} é um arquivo, não uma pasta.`));
298
+ break;
299
+ }
300
+
301
+ // A home e a raiz do disco são recusadas por padrão em qualquer
302
+ // lugar do código — aqui o pedido é explícito, então pergunta em vez
303
+ // de recusar, mas pergunta mesmo: é onde um write_file erra feio.
304
+ if (!isUsableCwd(alvo)) {
305
+ console.log(c.warn(` ${shortPath(alvo)} é a sua home ou a raiz do disco.`));
306
+ if (!(await confirm('O agente pode criar e editar arquivos aí. Confirma?'))) {
307
+ console.log(c.muted(' Nada mudou.'));
308
+ break;
309
+ }
310
+ }
311
+
312
+ state.workDir = alvo;
313
+ state.currentProject = path.basename(alvo);
314
+ state.workDirOrigem = '/dir';
315
+ const quantos = (() => { try { return fs.readdirSync(alvo).length; } catch { return 0; } })();
316
+ console.log(c.ok(` Trabalhando em ${shortPath(alvo)}`) + c.muted(` (${quantos} ${quantos === 1 ? 'item' : 'itens'})`));
317
+ refreshPrompt();
318
+ break;
319
+ }
320
+
240
321
  case 'mode': {
241
322
  const m = rest.toLowerCase();
242
- if (['primocode', 'chat'].includes(m)) { state.mode = m; console.log(c.ok(`Modo: ${m}`)); refreshPrompt(); }
243
- else console.log(c.err('Uso: /mode primocode|chat'));
323
+ if (['primocode', 'chat', 'auto'].includes(m)) {
324
+ state.mode = m;
325
+ state.modoEfetivo = null;
326
+ cfg.saveConfig({ mode: m });
327
+ console.log(c.ok(`Modo: ${m}`) + (m === 'auto'
328
+ ? c.muted(' — escolho por pedido: conversa vai pro chat, tarefa vai pro agente')
329
+ : ''));
330
+ refreshPrompt();
331
+ } else console.log(c.err('Uso: /mode primocode|chat|auto'));
244
332
  break;
245
333
  }
246
334
 
247
335
  case 'model': {
248
336
  const m = rest.toLowerCase();
249
- if (['main', 'fast'].includes(m)) { state.config.model = m; cfg.saveConfig({ model: m }); console.log(c.ok(`Modelo: ${m}`)); }
250
- else console.log(c.err('Uso: /model main|fast'));
337
+ 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',
341
+ };
342
+ if (nomes[m]) {
343
+ state.config.model = m;
344
+ cfg.saveConfig({ model: m });
345
+ console.log(c.ok(`Modelo: ${m}`) + c.muted(` ${nomes[m]}`));
346
+ } else if (m.includes('/')) {
347
+ // ID literal do OpenRouter, para quem quer sair dos três.
348
+ state.config.model = m;
349
+ cfg.saveConfig({ model: m });
350
+ console.log(c.ok(`Modelo: ${m}`) + c.muted(' precisa aceitar tools para o modo primocode'));
351
+ } else console.log(c.err('Uso: /model top|main|fast (ou um ID do OpenRouter, ex: google/gemini-2.5-pro)'));
251
352
  break;
252
353
  }
253
354
 
@@ -388,8 +489,13 @@ async function main() {
388
489
  if (isUsableCwd(process.cwd())) {
389
490
  state.workDir = process.cwd();
390
491
  state.currentProject = path.basename(state.workDir);
492
+ state.workDirOrigem = 'cwd';
391
493
  }
392
494
 
495
+ // Modo guardado entre sessões. O padrão é auto: sem ele, um "oi" gasta o
496
+ // mesmo que uma refatoração.
497
+ state.mode = ['primocode', 'chat', 'auto'].includes(state.config.mode) ? state.config.mode : 'auto';
498
+
393
499
  const args = process.argv.slice(2);
394
500
 
395
501
  // Flags padrão de CLI — funcionam em qualquer máquina, úteis em scripts/CI.
@@ -398,7 +504,7 @@ async function main() {
398
504
  process.exit(0);
399
505
  }
400
506
  if (args[0] === '--help' || args[0] === '-h') {
401
- console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName }));
507
+ console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName, modo: state.mode }));
402
508
  console.log(HELP);
403
509
  process.exit(0);
404
510
  }
@@ -431,6 +537,7 @@ async function main() {
431
537
  }
432
538
 
433
539
  console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null, {
540
+ modo: state.mode,
434
541
  mascot: state.config.mascot,
435
542
  userName: state.config.userName,
436
543
  }));
package/lib/config.js CHANGED
@@ -166,6 +166,8 @@ function loadConfig() {
166
166
  userName: file.userName || null,
167
167
  mascot: file.mascot || 'primo',
168
168
  theme: file.theme || 'primo',
169
+ // Modo escolhido dura entre sessões: quem liga o auto quer o auto amanhã.
170
+ mode: process.env.PRIMOCODE_MODE || file.mode || 'auto',
169
171
  rootDir: ROOT_DIR,
170
172
  _file: file,
171
173
  };
@@ -175,8 +177,8 @@ function saveConfig(partial) {
175
177
  ensureDir(CONFIG_DIR);
176
178
  const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
177
179
  // Só persiste o que faz sentido guardar — nada de campos derivados.
178
- const { server, model, token, autoApply, currentProject, userName, mascot, theme } = { ...cur, ...partial };
179
- const next = { server, model, token, autoApply, currentProject, userName, mascot, theme };
180
+ const { server, model, token, autoApply, currentProject, userName, mascot, theme, mode } = { ...cur, ...partial };
181
+ const next = { server, model, token, autoApply, currentProject, userName, mascot, theme, mode };
180
182
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
181
183
  return next;
182
184
  }
package/lib/modo.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Escolha automática de modo.
3
+ *
4
+ * O modo primocode manda o schema das 30 ferramentas em todo pedido — ~4.700
5
+ * tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que uma
6
+ * refatoração, e num plano com teto diário isso é dinheiro jogado fora (foi
7
+ * assim que a cota de 100 mil tokens/dia acabou às 15h).
8
+ *
9
+ * O modo chat não manda schema nenhum. Então a regra é: só paga o preço das
10
+ * ferramentas quem vai usar ferramenta.
11
+ *
12
+ * A decisão é local e sem custo — nenhuma chamada ao modelo para descobrir a
13
+ * intenção, senão o remédio custaria como a doença. É um classificador de
14
+ * pistas, deliberadamente conservador: na dúvida, chat, porque errar para chat
15
+ * custa uma repetição do pedido, e errar para primocode custa tokens e pode
16
+ * escrever arquivo que ninguém pediu.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ // Verbos de tarefa no imperativo e no infinitivo, como as pessoas falam de
22
+ // verdade: "cria um site", "criar um site", "crie um site", "faz pra mim".
23
+ const VERBOS = [
24
+ 'cri[ae]r?', 'faz(er)?', 'fa[çc]a', 'escrev[ae]r?', 'ger[ae]r?', 'monta?r?',
25
+ 'consert[ae]r?', 'corrig[ei]r?', 'arrum[ae]r?', 'ajust[ae]r?',
26
+ 'rod[ae]r?', 'execut[ae]r?', 'instal[ae]r?', 'test[ae]r?',
27
+ 'abr[ae]?i?r?', 'clon[ae]r?', 'baix[ae]r?', 'public[ao]r?',
28
+ 'refator[ae]r?', 'apag[ae]r?', 'delet[ae]r?', 'remov[ae]r?', 'mov[ae]r?',
29
+ 'adicion[ae]r?', 'acrescent[ae]r?', 'mud[ae]r?', 'alter[ae]r?', 'atualiz[ae]r?',
30
+ 'implement[ae]r?', 'l[eê]i?a?r?', 'procur[ae]r?', 'busc[ae]r?', 'encontr[ae]r?',
31
+ 'renome[ai]r?', 'copi[ae]r?', 'commit[ae]r?', 'deploy',
32
+ 'sobe', 'suba', 'subir', 'empurr[ae]r?', 'mergea?r?',
33
+ ];
34
+ const RE_VERBO = new RegExp(`(^|\\s)(${VERBOS.join('|')})\\b`, 'i');
35
+
36
+ // Coisas que só existem quando há trabalho de arquivo/máquina envolvido.
37
+ const RE_ARTEFATO = /\.(js|ts|tsx|jsx|html|css|json|md|py|sh|yml|yaml|txt|csv|env)\b|[\w-]+\/[\w-]/i;
38
+ const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela)\b/i;
39
+ const RE_PASTA = /\b(pasta|diret[óo]rio|arquivo|arquivos|reposit[óo]rio|repo|projeto)\b/i;
40
+
41
+ // Conversa: cumprimento, agradecimento, pergunta conceitual.
42
+ const RE_SAUDACAO = /^\s*(oi+|ol[áa]|e?a[íi]|bom dia|boa tarde|boa noite|tudo bem|beleza|valeu|obrigad[oa]|brigad[oa]|tchau|até|ok|okay|entendi|legal|show|massa)\b[\s!.?]*$/i;
43
+ const RE_CONCEITUAL = /\b(o que [ée]|o q [ée]|que [ée]|qual (a|o|as|os)?\s*(diferen[çc]a|melhor|sentido)|por qu[êe]|pra que serve|para que serve|como funciona|como (fa[çc]o|posso|se faz|fazer)|explica|explique|me explica|significa|vale a pena|devo usar|qual usar|prefere)\b/i;
44
+
45
+ /**
46
+ * Decide o modo de um pedido.
47
+ * Devolve { modo: 'primocode'|'chat', motivo: string }.
48
+ * O motivo existe para o CLI mostrar por que escolheu — automode que decide em
49
+ * silêncio parece defeito na primeira vez que erra.
50
+ */
51
+ function escolherModo(texto) {
52
+ const t = String(texto || '').trim();
53
+ if (!t) return { modo: 'chat', motivo: 'nada a fazer' };
54
+
55
+ // Cumprimento é chat mesmo que traga um verbo depois ("oi, tudo bem?").
56
+ if (RE_SAUDACAO.test(t)) return { modo: 'chat', motivo: 'cumprimento' };
57
+
58
+ // Pergunta conceitual ganha do verbo: "como faço para criar um site?" é
59
+ // dúvida, não pedido de execução.
60
+ if (RE_CONCEITUAL.test(t)) return { modo: 'chat', motivo: 'pergunta conceitual' };
61
+
62
+ if (RE_ARTEFATO.test(t)) return { modo: 'primocode', motivo: 'cita arquivo ou caminho' };
63
+ if (RE_VERBO.test(t)) return { modo: 'primocode', motivo: 'pede uma ação' };
64
+ if (RE_FERRAMENTA.test(t)) return { modo: 'primocode', motivo: 'menciona ferramenta ou máquina' };
65
+ if (RE_PASTA.test(t)) return { modo: 'primocode', motivo: 'fala de arquivo ou projeto' };
66
+
67
+ return { modo: 'chat', motivo: 'sem pista de tarefa' };
68
+ }
69
+
70
+ module.exports = { escolherModo };
package/lib/ui.js CHANGED
@@ -6,17 +6,33 @@
6
6
 
7
7
  const readline = require('readline');
8
8
 
9
+ // ── Paleta ───────────────────────────────────────────────────────────────
10
+ // Vem do design system do produto (product-plan/design-system): primária cyan,
11
+ // secundária amber, neutra slate, e rose para falha de ação. Os números são o
12
+ // vizinho mais próximo de cada degrau do Tailwind no cubo de 256 cores do
13
+ // terminal, calculado com peso perceptual — não escolhido a olho.
14
+ //
15
+ // brand cyan-400 #22d3ee → 45 o que foi apurado, confirmado
16
+ // brandDim cyan-600 #0891b2 → 31 a mesma cor recuando
17
+ // white slate-200 #e2e8f0 → 254 texto
18
+ // muted slate-400 #94a3b8 → 247 texto secundário
19
+ // dim slate-500 #64748b → 243 borda, apoio
20
+ // warn amber-400 #fbbf24 → 214 aviso que não é falha
21
+ // err rose-400 #fb7185 → 204 falha de ação
22
+ //
23
+ // O terminal é fundo escuro, então os degraus usados são os da coluna `dark:`
24
+ // da tabela de contraste validada do design system.
9
25
  const PALETTE = {
10
- brand: '\x1b[38;5;183m',
11
- brandDim: '\x1b[38;5;146m',
12
- white: '\x1b[38;5;253m',
13
- muted: '\x1b[38;5;245m',
14
- dim: '\x1b[38;5;240m',
15
- ok: '\x1b[38;5;114m',
16
- warn: '\x1b[38;5;180m',
17
- err: '\x1b[38;5;174m',
18
- magenta: '\x1b[38;5;176m',
19
- cyan: '\x1b[38;5;152m',
26
+ brand: '\x1b[38;5;45m',
27
+ brandDim: '\x1b[38;5;31m',
28
+ white: '\x1b[38;5;254m',
29
+ muted: '\x1b[38;5;247m',
30
+ dim: '\x1b[38;5;243m',
31
+ ok: '\x1b[38;5;45m',
32
+ warn: '\x1b[38;5;214m',
33
+ err: '\x1b[38;5;204m',
34
+ magenta: '\x1b[38;5;214m',
35
+ cyan: '\x1b[38;5;45m',
20
36
  bold: '\x1b[1m',
21
37
  reset: '\x1b[0m',
22
38
  };
@@ -58,10 +74,10 @@ const PES = ' ▘▘ ▝▝ ';
58
74
  const MASCOTS = {
59
75
  // Padrão: o Primo — cabeça cheia, ombros marcados, dois pezinhos.
60
76
  primo: { topo: ' ▐▛███▜▌ ', full: 'brand', edge: 'brandDim' },
61
- // Raposa — orelhas largas, abertas para fora.
77
+ // Raposa — orelhas largas, abertas para fora. Na secundária do sistema.
62
78
  fox: { topo: ' ▛▀ ▀▜ ', full: 'warn', edge: 'dim' },
63
- // Gato — orelhas curtas e juntas.
64
- cat: { topo: ' ▛▘ ▝▜ ', full: 'cyan', edge: 'brandDim' },
79
+ // Gato — orelhas curtas e juntas. Primária recuada, para não repetir o Primo.
80
+ cat: { topo: ' ▛▘ ▝▜ ', full: 'brandDim', edge: 'dim' },
65
81
  };
66
82
 
67
83
  // Bloco cheio na cor viva, contorno na apagada: dá volume ao desenho sem
@@ -140,14 +156,14 @@ const NOME = 'Primo Code';
140
156
 
141
157
  const TIPS = [
142
158
  'Peça um app novo, ou aponte para um repositório e mande clonar',
143
- 'Ele controla navegador e desktop de verdade — peça e veja',
144
- '/help lista tudo · /mascot troca o mascote · /status confere a API',
159
+ '/dir <pasta> para ele trabalhar numa pasta que já é sua',
160
+ '/help lista tudo · /mode troca o modo · /status confere a API',
145
161
  ];
146
162
 
147
163
  const NEWS = [
148
- 'Cabeçalho novo: mascote em blocos e identidade em três linhas',
149
- 'Mascotes redesenhados na mesma família — primo, fox e cat',
150
- 'Instalador detecta e instala o Node sozinho, sem sudo e sem EACCES',
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',
151
167
  ];
152
168
 
153
169
  // ── Cabeçalho ────────────────────────────────────────────────────────────
@@ -156,7 +172,7 @@ const NEWS = [
156
172
  // alinhamento do desenho já dá a borda esquerda de graça.
157
173
 
158
174
  function welcomeBox(version, contexto, opts = {}) {
159
- const { mascot: style, userName } = opts;
175
+ const { mascot: style, userName, modo } = opts;
160
176
  const art = mascot(style);
161
177
  const cols = Math.max(32, Math.min(process.stdout.columns || 100, 120));
162
178
 
@@ -200,13 +216,20 @@ function welcomeBox(version, contexto, opts = {}) {
200
216
  secao('Dicas para começar', TIPS, (i) => `${i + 1}.`);
201
217
  secao('Novidades', NEWS, () => ' ·');
202
218
 
203
- // Rodapé de modos. Em terminal apertado a dica do /help sai antes de
204
- // empurrar a linha para baixo — cabeçalho que quebra parece defeito.
205
- const modos =
206
- ` ${c.magenta('●')} ${c.muted('Primo Code')}` +
207
- ` ${c.brand('●')} ${c.muted('Chat')}`;
219
+ // Rodapé: em que modo você está agora, não a lista dos que existem. Em
220
+ // terminal apertado a dica do /help sai antes de empurrar a linha para
221
+ // baixo — cabeçalho que quebra parece defeito.
222
+ // Primária para o modo ativo, secundária quando ele tem poder de escrever:
223
+ // é a mesma leitura de "apurado" × "aviso que não é falha" do sistema.
224
+ const rotulos = {
225
+ auto: `${c.brand('●')} ${c.muted('Auto')}${c.dim(' — conversa vai pro chat, tarefa vai pro agente')}`,
226
+ primocode: `${c.warn('●')} ${c.muted('Primo Code')}${c.dim(' — agente completo, com ferramentas')}`,
227
+ chat: `${c.brandDim('●')} ${c.muted('Chat')}${c.dim(' — conversa pura, sem ferramentas')}`,
228
+ };
229
+ const linhaModo = ' ' + (rotulos[modo] || rotulos.auto);
208
230
  out.push('');
209
- out.push(cols >= 44 ? `${modos} ${c.dim('/help para tudo')}` : modos);
231
+ out.push(visibleWidth(linhaModo) <= cols ? linhaModo : ` ${c.ok('●')} ${c.muted(modo || 'auto')}`);
232
+ if (cols >= 44) out.push(` ${c.dim('/help para tudo · /mode troca o modo')}`);
210
233
  out.push('');
211
234
  return out.join('\n');
212
235
  }
@@ -282,7 +305,7 @@ function actionHeader(name, args) {
282
305
  return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
283
306
  }
284
307
 
285
- // Transforma erro cru (JSON do Groq, código HTTP, erro de rede) numa frase
308
+ // Transforma erro cru (JSON do provedor, código HTTP, erro de rede) numa frase
286
309
  // humana. O usuário nunca deveria ver um paredão de JSON.
287
310
  function friendlyError(raw) {
288
311
  const s = String(raw && raw.message ? raw.message : raw || '').trim();
@@ -298,10 +321,48 @@ function friendlyError(raw) {
298
321
  }
299
322
  } catch {}
300
323
 
301
- if (/rate.?limit|tokens per minute|tpm|request too large|too many requests|\b429\b|\b413\b/i.test(low)) {
302
- return '⏳ O servidor está no limite de uso agora. Espere alguns segundos e tente de novo.';
324
+ // Falta de saldo: nem bug, nem espera. O provedor diz quanto a conta paga,
325
+ // e o servidor já refaz o pedido com esse teto — se ainda assim chegou aqui,
326
+ // é porque nem o mínimo cabe.
327
+ if (/can only afford|requires more credits|\b402\b/i.test(low)) {
328
+ const q = s.match(/afford\s+(\d+)/i);
329
+ return '💳 A conta do OpenRouter não tem crédito para uma resposta deste tamanho'
330
+ + (q ? ` (dá para ${q[1]} tokens)` : '')
331
+ + '. Adicione crédito em openrouter.ai/settings/credits, ou use /model fast'
332
+ + ' para pedidos menores.';
333
+ }
334
+ // Ordem importa: "grande demais" e "muitos pedidos" chegam os dois como
335
+ // rate_limit_exceeded, e a saída é oposta. Tamanho não se resolve
336
+ // esperando — o pedido vai estourar igual daqui a uma hora.
337
+ const porTamanho = /request too large|tokens per minute|\btpm\b|reduce your message size|\b413\b/i.test(low)
338
+ || /conversa passou do teto/i.test(low);
339
+ if (porTamanho) {
340
+ const nums = s.match(/Limit\s+(\d+),\s*Requested\s+(\d+)/i);
341
+ if (/conversa passou do teto/i.test(low)) return `📏 ${inner.replace(/\s+/g, ' ').trim()}`;
342
+ return '📏 Este pedido é maior que o teto do plano'
343
+ + (nums ? ` (${nums[2]} tokens contra um limite de ${nums[1]} por minuto)` : '')
344
+ + '. Esperar não resolve: use /clear para encurtar a conversa, ou divida o pedido em partes menores.';
345
+ }
346
+ // Cota do dia estourada. Nada a fazer no código, mas o provedor diz quanto
347
+ // falta para liberar — jogar isso fora e escrever "alguns instantes" é
348
+ // mandar o usuário tentar de novo em vão por uma hora e meia.
349
+ if (/tokens per day|\btpd\b/i.test(low)) {
350
+ // A classe [0-9hms.] engole o ponto final da frase do provedor, então tira
351
+ // a pontuação antes de arredondar os milissegundos fora.
352
+ const bruto = (s.match(/try again in ([0-9hms.]+)/i) || [])[1];
353
+ const quando = bruto && bruto.replace(/\.+$/, '').replace(/\.\d+s$/, 's');
354
+ const uso = s.match(/Limit\s+(\d+),\s*Used\s+(\d+)/i);
355
+ return '📅 A cota de tokens do dia acabou'
356
+ + (uso ? ` (${uso[2]} de ${uso[1]} tokens)` : '')
357
+ + (quando ? `, libera em ${quando}` : '')
358
+ + '. Enquanto isso o /mode chat gasta bem menos por pergunta.';
359
+ }
360
+ if (/rate.?limit|too many requests|\b429\b/i.test(low)) {
361
+ return '⏳ Muitos pedidos em pouco tempo. Espere alguns instantes e tente de novo.';
303
362
  }
304
- if (/\b401\b|unauthorized|token/i.test(low)) {
363
+ // "token" solto não serve de pista: a mensagem de TPM tem a palavra e não
364
+ // tem nada a ver com autenticação. Exige o código ou a frase inteira.
365
+ if (/\b401\b|unauthorized|forbidden|token (inv[aá]lido|ausente|expirado)|invalid token|missing token/i.test(low)) {
305
366
  return '🔒 Este servidor pede um token. Configure PRIMOCODE_TOKEN ou "token" em ~/.primocode/config.json.';
306
367
  }
307
368
  if (/econnrefused|enotfound|etimedout|timeout|socket hang up|network|getaddrinfo|econnreset/i.test(low)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "7.5.0",
3
+ "version": "7.7.0",
4
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.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"