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 +43 -9
- package/bin/primocode.js +121 -14
- package/lib/config.js +4 -2
- package/lib/modo.js +70 -0
- package/lib/ui.js +90 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# PrimoCode v7.
|
|
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.
|
|
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
|
-
| **
|
|
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
|
-
| **
|
|
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
|
-
| `/
|
|
209
|
-
| `/
|
|
210
|
-
| `/
|
|
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 │ ────────► │
|
|
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,
|
|
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('/
|
|
38
|
-
${c.brand('/
|
|
39
|
-
${c.brand('/
|
|
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.
|
|
56
|
-
${c.
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|
|
243
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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;
|
|
11
|
-
brandDim: '\x1b[38;5;
|
|
12
|
-
white: '\x1b[38;5;
|
|
13
|
-
muted: '\x1b[38;5;
|
|
14
|
-
dim: '\x1b[38;5;
|
|
15
|
-
ok: '\x1b[38;5;
|
|
16
|
-
warn: '\x1b[38;5;
|
|
17
|
-
err: '\x1b[38;5;
|
|
18
|
-
magenta: '\x1b[38;5;
|
|
19
|
-
cyan: '\x1b[38;5;
|
|
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: '
|
|
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
|
-
'
|
|
144
|
-
'/help lista tudo · /
|
|
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
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
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
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|