primocode 7.5.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 +64 -10
- package/bin/primocode.js +256 -37
- package/lib/act.js +21 -9
- package/lib/browser.js +5 -1
- package/lib/catalogo.js +94 -0
- package/lib/config.js +4 -2
- package/lib/modo.js +70 -0
- package/lib/pasta.js +187 -0
- package/lib/ui.js +110 -30
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
# PrimoCode v7.
|
|
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.
|
|
12
|
+
▐▛███▜▌ Primo Code v7.8.0
|
|
9
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
10
|
-
▘▘ ▝▝ ~/
|
|
14
|
+
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
11
15
|
```
|
|
12
16
|
|
|
13
17
|
Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
|
|
@@ -23,7 +27,7 @@ veja `/mascot` e `/name`.
|
|
|
23
27
|
| **Memória** | Cada projeto lembra o que foi feito, entre sessões | Esquece ao fechar |
|
|
24
28
|
| **Instalação** | Node puro, **zero dependências** — segundos | Pacotes e dependências |
|
|
25
29
|
| **Cara própria** | Mascote, nome e temas que você escolhe | Fixo |
|
|
26
|
-
| **
|
|
30
|
+
| **Modelos** | Claude Opus 5, Sonnet 5 e Haiku 4.5 via OpenRouter — trocáveis com `/model` | Preso ao provedor do fabricante |
|
|
27
31
|
|
|
28
32
|
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
33
|
|
|
@@ -93,10 +97,58 @@ fim. É por isso que a IA não esquece o projeto entre sessões.
|
|
|
93
97
|
|
|
94
98
|
| Modo | O que faz |
|
|
95
99
|
|---|---|
|
|
96
|
-
| **
|
|
100
|
+
| **Auto** (padrão) | Escolhe por pedido: conversa vai pro chat, tarefa vai pro agente |
|
|
101
|
+
| **Primo Code** | Agente completo — cria arquivos, roda comandos, abre o navegador |
|
|
97
102
|
| **Chat** | Conversa pura, sem tools |
|
|
98
103
|
|
|
99
|
-
Alterne com `/mode primocode` ou `/mode chat`.
|
|
104
|
+
Alterne com `/mode auto`, `/mode primocode` ou `/mode chat`. A escolha fica
|
|
105
|
+
guardada entre sessões.
|
|
106
|
+
|
|
107
|
+
**Por que o auto é o padrão.** O modo Primo Code manda o catálogo das 30
|
|
108
|
+
ferramentas em todo pedido — cerca de 4.700 tokens antes de você digitar a
|
|
109
|
+
primeira letra. Um "oi" custava o mesmo que uma refatoração, e num plano com
|
|
110
|
+
teto diário isso acaba a cota no meio da tarde. O auto decide na sua máquina,
|
|
111
|
+
sem chamar o modelo para isso, e mostra o que escolheu:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
Auto ❯ oi
|
|
115
|
+
⎿ Chat · cumprimento
|
|
116
|
+
|
|
117
|
+
Auto·chat ❯ conserta o bug do login em src/auth.js
|
|
118
|
+
⎿ Primo Code · cita arquivo ou caminho
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Na dúvida ele vai para o chat: errar para chat custa repetir o pedido, errar
|
|
122
|
+
para o agente custa token e pode escrever arquivo que ninguém pediu.
|
|
123
|
+
|
|
124
|
+
## Onde ele trabalha
|
|
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
|
+
|
|
133
|
+
| Situação | Pasta de trabalho |
|
|
134
|
+
|---|---|
|
|
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 |
|
|
136
|
+
| `/dir ~/meus-projetos/site` | a pasta que **você** escolher, onde ela estiver |
|
|
137
|
+
| `/project loja` | cria `~/primocode/loja`, com memória própria |
|
|
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
|
+
|
|
147
|
+
`/dir` sem argumento mostra a pasta ativa e de onde ela veio. Caminho relativo
|
|
148
|
+
resolve contra a pasta ativa, então depois de um `/dir` o `/dir src` entra na
|
|
149
|
+
subpasta. Se a pasta não existir, ele pergunta antes de criar; se for a sua home
|
|
150
|
+
ou a raiz do disco, pergunta de novo — é onde um arquivo escrito por engano dói
|
|
151
|
+
mais.
|
|
100
152
|
|
|
101
153
|
## Ferramentas do agente
|
|
102
154
|
|
|
@@ -205,11 +257,13 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
|
|
|
205
257
|
|
|
206
258
|
| Comando | Descrição |
|
|
207
259
|
|---|---|
|
|
208
|
-
| `/
|
|
209
|
-
| `/
|
|
210
|
-
| `/
|
|
260
|
+
| `/dir <caminho>` | Trabalha numa pasta que já existe, onde você quiser |
|
|
261
|
+
| `/project` | Lista projetos. `/project <nome>` cria em `~/primocode` |
|
|
262
|
+
| `/mode primocode\|chat\|auto` | Alterna o modo (fica guardado) |
|
|
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 |
|
|
211
264
|
| `/attach <arquivo>` | Anexa arquivo ao próximo pedido |
|
|
212
265
|
| `/detach` · `/files` | Remove / lista anexos |
|
|
266
|
+
| `/tudo` | Lista as 30 ferramentas que ele sabe usar, por grupo |
|
|
213
267
|
| `/tree [n]` | Árvore do projeto atual |
|
|
214
268
|
| `/read <arquivo>` | Mostra um arquivo do projeto |
|
|
215
269
|
| `/run <comando>` | Executa no shell, dentro da pasta do projeto |
|
|
@@ -252,7 +306,7 @@ Caminho absoluto ou com `../` é recusado — o agente não escreve fora da sua
|
|
|
252
306
|
|
|
253
307
|
```
|
|
254
308
|
┌────────────────┐ HTTPS/SSE ┌───────────────────┐ HTTPS ┌──────────┐
|
|
255
|
-
│ primocode CLI │ ─────────────► │ primocode-server │ ────────► │
|
|
309
|
+
│ primocode CLI │ ─────────────► │ primocode-server │ ────────► │OpenRouter│
|
|
256
310
|
│ (seu terminal)│ ◄───────────── │ (Render) │ ◄──────── │ API │
|
|
257
311
|
└────────────────┘ └───────────────────┘ └──────────┘
|
|
258
312
|
executa as tools monta o prompt
|
package/bin/primocode.js
CHANGED
|
@@ -15,30 +15,42 @@ 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');
|
|
21
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();
|
|
22
30
|
|
|
23
31
|
const VERSION = pkg.version;
|
|
24
32
|
|
|
25
33
|
const state = {
|
|
26
34
|
config: cfg.loadConfig(),
|
|
27
35
|
history: [],
|
|
28
|
-
mode: 'primocode'
|
|
36
|
+
mode: null, // 'primocode' | 'chat' | 'auto' — vem da config em main()
|
|
37
|
+
modoEfetivo: null, // no auto, o modo que o último pedido usou de fato
|
|
29
38
|
attachedFiles: [],
|
|
30
39
|
lastResponse: '',
|
|
31
40
|
currentProject: null,
|
|
32
|
-
workDir: null,
|
|
41
|
+
workDir: null, // pasta onde o agente trabalha nesta sessão
|
|
42
|
+
workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
|
|
33
43
|
};
|
|
34
44
|
|
|
35
45
|
const HELP = `
|
|
36
46
|
${c.bold('Comandos')}
|
|
37
|
-
${c.brand('/
|
|
38
|
-
${c.brand('/
|
|
39
|
-
${c.brand('/
|
|
47
|
+
${c.brand('/dir')} <caminho> trabalha numa pasta que já existe (onde você quiser)
|
|
48
|
+
${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
|
|
49
|
+
${c.brand('/mode')} /mode primocode|chat|auto
|
|
50
|
+
${c.brand('/model')} /model top|main|fast — ou um ID do OpenRouter
|
|
40
51
|
${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
|
|
41
52
|
${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
|
|
53
|
+
${c.brand('/tudo')} lista tudo que ele sabe fazer
|
|
42
54
|
${c.brand('/tree')} [n] árvore do projeto
|
|
43
55
|
${c.brand('/read')} <arquivo> mostra um arquivo
|
|
44
56
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
@@ -52,14 +64,30 @@ ${c.bold('Comandos')}
|
|
|
52
64
|
${c.brand('/exit')} sair
|
|
53
65
|
|
|
54
66
|
${c.bold('Modos')}
|
|
55
|
-
${c.
|
|
56
|
-
${c.
|
|
67
|
+
${c.warn('Primo Code')} ${c.muted('Agente completo — cria arquivos, roda comandos, abre o navegador')}
|
|
68
|
+
${c.brandDim('Chat')} ${c.muted('Conversa pura — sem tools, só diálogo. Gasta bem menos token')}
|
|
69
|
+
${c.brand('Auto')} ${c.muted('Escolhe por pedido: conversa vai pro chat, tarefa vai pro agente')}
|
|
70
|
+
|
|
71
|
+
${c.bold('Onde ele trabalha')}
|
|
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.')}
|
|
57
77
|
`;
|
|
58
78
|
|
|
59
79
|
let rl = null;
|
|
60
80
|
|
|
61
81
|
function makePrompt() {
|
|
62
|
-
|
|
82
|
+
// No auto o rótulo mostra "Auto" e, depois do primeiro pedido, qual modo
|
|
83
|
+
// ele acabou usando — senão você nunca sabe quem respondeu.
|
|
84
|
+
let modeTag;
|
|
85
|
+
if (state.mode === 'auto') {
|
|
86
|
+
const ultimo = state.modoEfetivo === 'primocode' ? c.warn('primo') : state.modoEfetivo === 'chat' ? c.brandDim('chat') : '';
|
|
87
|
+
modeTag = c.brand('Auto') + (ultimo ? c.dim('·') + ultimo : '');
|
|
88
|
+
} else {
|
|
89
|
+
modeTag = state.mode === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
|
|
90
|
+
}
|
|
63
91
|
const proj = state.currentProject ? c.dim(` ${state.currentProject}`) : '';
|
|
64
92
|
const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
|
|
65
93
|
return `\n${c.muted(modeTag)}${proj}${att} ${c.brand('❯')} `;
|
|
@@ -103,32 +131,41 @@ function deriveProjectName(text) {
|
|
|
103
131
|
return picked || 'projeto';
|
|
104
132
|
}
|
|
105
133
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
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) {
|
|
110
139
|
const home = os.homedir();
|
|
111
140
|
const resolved = path.resolve(dir);
|
|
112
141
|
if (resolved === path.resolve(home)) return false;
|
|
113
142
|
if (resolved === path.parse(resolved).root) return false;
|
|
114
|
-
if (resolved.startsWith(path.join(home, 'primocode'))) return false;
|
|
115
143
|
return true;
|
|
116
144
|
}
|
|
117
145
|
|
|
118
|
-
// Escolhe onde trabalhar SEM criar nada.
|
|
119
|
-
//
|
|
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.
|
|
120
154
|
function planWorkDir(userText) {
|
|
121
|
-
if (state.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
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 };
|
|
126
162
|
}
|
|
127
163
|
|
|
128
164
|
function createPendingProject(plan) {
|
|
129
165
|
const dir = cfg.createProjectDir(plan.name);
|
|
130
166
|
state.workDir = dir;
|
|
131
167
|
state.currentProject = path.basename(dir);
|
|
168
|
+
state.workDirOrigem = 'projeto';
|
|
132
169
|
const rotulo = plan.similar ? 'Continuando em' : 'Projeto novo:';
|
|
133
170
|
console.log(c.ok(` ${rotulo} ${state.currentProject}`) + c.muted(` (${dir})`));
|
|
134
171
|
refreshPrompt();
|
|
@@ -141,6 +178,63 @@ function projectDir() {
|
|
|
141
178
|
|
|
142
179
|
// ── Envio ──────────────────────────────────────────────────────────────
|
|
143
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
|
+
|
|
144
238
|
async function sendPrompt(userText) {
|
|
145
239
|
let prompt = userText;
|
|
146
240
|
if (state.attachedFiles.length) {
|
|
@@ -148,16 +242,30 @@ async function sendPrompt(userText) {
|
|
|
148
242
|
prompt = parts.join('\n\n') + '\n\n' + userText;
|
|
149
243
|
}
|
|
150
244
|
|
|
151
|
-
|
|
152
|
-
|
|
245
|
+
// O modo primocode manda o schema das 30 tools em todo pedido (~4.700
|
|
246
|
+
// tokens). No auto, quem não vai usar ferramenta não paga por ela.
|
|
247
|
+
let modo = state.mode;
|
|
248
|
+
if (modo === 'auto') {
|
|
249
|
+
const escolha = escolherModo(userText);
|
|
250
|
+
modo = escolha.modo;
|
|
251
|
+
state.modoEfetivo = modo;
|
|
252
|
+
const rotulo = modo === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
|
|
253
|
+
console.log(c.dim(` ⎿ ${ui.stripAnsi(rotulo)} · ${escolha.motivo}`));
|
|
254
|
+
refreshPrompt();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (modo === 'primocode') {
|
|
258
|
+
const plan = prepararPasta(userText);
|
|
259
|
+
|
|
153
260
|
const r = await runAct(prompt, {
|
|
154
261
|
server: state.config.server,
|
|
155
262
|
model: state.config.model,
|
|
156
263
|
token: state.config.token,
|
|
157
264
|
history: state.history,
|
|
158
265
|
projectDir: plan.dir,
|
|
159
|
-
projectName: plan.
|
|
160
|
-
// Só chamado se o agente for mesmo
|
|
266
|
+
projectName: plan.name || state.currentProject,
|
|
267
|
+
// Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
|
|
268
|
+
// não é motivo para criar pasta de projeto.
|
|
161
269
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
162
270
|
});
|
|
163
271
|
state.history.push({ role: 'user', content: userText });
|
|
@@ -232,22 +340,99 @@ async function handleSlash(text) {
|
|
|
232
340
|
const dir = cfg.createProjectDir(rest);
|
|
233
341
|
state.workDir = dir;
|
|
234
342
|
state.currentProject = path.basename(dir);
|
|
343
|
+
state.workDirOrigem = 'projeto';
|
|
235
344
|
console.log(c.ok(`Projeto: ${state.currentProject}`) + c.muted(` (${dir})`));
|
|
236
345
|
refreshPrompt();
|
|
237
346
|
break;
|
|
238
347
|
}
|
|
239
348
|
|
|
349
|
+
// Antes só existia /project, que SEMPRE cria em ~/primocode/<nome>. Se a
|
|
350
|
+
// sua pasta já existe em outro lugar, não havia como apontar para ela.
|
|
351
|
+
case 'dir':
|
|
352
|
+
case 'cd':
|
|
353
|
+
case 'pasta': {
|
|
354
|
+
if (!rest) {
|
|
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
|
+
};
|
|
361
|
+
console.log(` ${c.white(shortPath(projectDir()))}`);
|
|
362
|
+
console.log(c.muted(` ${de[state.workDirOrigem] || 'pasta atual do terminal'}`));
|
|
363
|
+
console.log(c.dim(' /dir <caminho> muda · /project <nome> cria uma nova em ~/primocode'));
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ~ e caminho relativo resolvem contra a pasta ativa, não contra o
|
|
368
|
+
// cwd do processo: depois de um /dir, "src" é dentro dela.
|
|
369
|
+
const bruto = rest.replace(/^~(?=$|[/\\])/, os.homedir());
|
|
370
|
+
const alvo = path.resolve(projectDir(), bruto);
|
|
371
|
+
|
|
372
|
+
if (!fs.existsSync(alvo)) {
|
|
373
|
+
if (!(await confirm(`A pasta ${shortPath(alvo)} não existe. Criar?`))) {
|
|
374
|
+
console.log(c.muted(' Nada mudou.'));
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
try { fs.mkdirSync(alvo, { recursive: true }); }
|
|
378
|
+
catch (e) { console.log(c.err(` Não consegui criar: ${e.message}`)); break; }
|
|
379
|
+
} else if (!fs.statSync(alvo).isDirectory()) {
|
|
380
|
+
console.log(c.err(` ${shortPath(alvo)} é um arquivo, não uma pasta.`));
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// A home e a raiz do disco são recusadas por padrão em qualquer
|
|
385
|
+
// lugar do código — aqui o pedido é explícito, então pergunta em vez
|
|
386
|
+
// de recusar, mas pergunta mesmo: é onde um write_file erra feio.
|
|
387
|
+
if (!pastaSegura(alvo)) {
|
|
388
|
+
console.log(c.warn(` ${shortPath(alvo)} é a sua home ou a raiz do disco.`));
|
|
389
|
+
if (!(await confirm('O agente pode criar e editar arquivos aí. Confirma?'))) {
|
|
390
|
+
console.log(c.muted(' Nada mudou.'));
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
state.workDir = alvo;
|
|
396
|
+
state.currentProject = path.basename(alvo);
|
|
397
|
+
state.workDirOrigem = '/dir';
|
|
398
|
+
const quantos = (() => { try { return fs.readdirSync(alvo).length; } catch { return 0; } })();
|
|
399
|
+
console.log(c.ok(` Trabalhando em ${shortPath(alvo)}`) + c.muted(` (${quantos} ${quantos === 1 ? 'item' : 'itens'})`));
|
|
400
|
+
refreshPrompt();
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
|
|
240
404
|
case 'mode': {
|
|
241
405
|
const m = rest.toLowerCase();
|
|
242
|
-
if (['primocode', 'chat'].includes(m)) {
|
|
243
|
-
|
|
406
|
+
if (['primocode', 'chat', 'auto'].includes(m)) {
|
|
407
|
+
state.mode = m;
|
|
408
|
+
state.modoEfetivo = null;
|
|
409
|
+
cfg.saveConfig({ mode: m });
|
|
410
|
+
console.log(c.ok(`Modo: ${m}`) + (m === 'auto'
|
|
411
|
+
? c.muted(' — escolho por pedido: conversa vai pro chat, tarefa vai pro agente')
|
|
412
|
+
: ''));
|
|
413
|
+
refreshPrompt();
|
|
414
|
+
} else console.log(c.err('Uso: /mode primocode|chat|auto'));
|
|
244
415
|
break;
|
|
245
416
|
}
|
|
246
417
|
|
|
247
418
|
case 'model': {
|
|
248
419
|
const m = rest.toLowerCase();
|
|
249
|
-
|
|
250
|
-
|
|
420
|
+
// Todos grátis. A lista viva de cada corrente está no /status.
|
|
421
|
+
const nomes = {
|
|
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',
|
|
425
|
+
};
|
|
426
|
+
if (nomes[m]) {
|
|
427
|
+
state.config.model = m;
|
|
428
|
+
cfg.saveConfig({ model: m });
|
|
429
|
+
console.log(c.ok(`Modelo: ${m}`) + c.muted(` ${nomes[m]}`) + c.dim(' · grátis'));
|
|
430
|
+
} else if (m.includes('/')) {
|
|
431
|
+
// ID literal do OpenRouter, para quem quer sair dos três.
|
|
432
|
+
state.config.model = m;
|
|
433
|
+
cfg.saveConfig({ model: m });
|
|
434
|
+
console.log(c.ok(`Modelo: ${m}`) + c.muted(' precisa aceitar tools para o modo primocode'));
|
|
435
|
+
} else console.log(c.err('Uso: /model top|main|fast (ou um ID do OpenRouter, ex: google/gemini-2.5-pro)'));
|
|
251
436
|
break;
|
|
252
437
|
}
|
|
253
438
|
|
|
@@ -269,6 +454,25 @@ async function handleSlash(text) {
|
|
|
269
454
|
else state.attachedFiles.forEach((f) => console.log(` ${c.brand(f.path)} ${c.muted(tools.fmtBytes(f.content.length))}`));
|
|
270
455
|
break;
|
|
271
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
|
+
|
|
272
476
|
case 'tree': console.log(tools.treeDir(projectDir(), parseInt(rest, 10) || 2)); break;
|
|
273
477
|
|
|
274
478
|
case 'read': {
|
|
@@ -350,7 +554,13 @@ async function handleSlash(text) {
|
|
|
350
554
|
sp.stop();
|
|
351
555
|
console.log(c.ok(`Servidor: ${h.status || 'ok'}`));
|
|
352
556
|
if (h.version) console.log(c.muted(` Versão: ${h.version}`));
|
|
353
|
-
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
|
+
}
|
|
354
564
|
} catch (e) { sp.stop(); console.log(c.err(`Indisponível: ${e.message}`)); }
|
|
355
565
|
break;
|
|
356
566
|
}
|
|
@@ -384,11 +594,16 @@ function shortPath(p) {
|
|
|
384
594
|
async function main() {
|
|
385
595
|
cfg.ensureStructure();
|
|
386
596
|
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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';
|
|
603
|
+
|
|
604
|
+
// Modo guardado entre sessões. O padrão é auto: sem ele, um "oi" gasta o
|
|
605
|
+
// mesmo que uma refatoração.
|
|
606
|
+
state.mode = ['primocode', 'chat', 'auto'].includes(state.config.mode) ? state.config.mode : 'auto';
|
|
392
607
|
|
|
393
608
|
const args = process.argv.slice(2);
|
|
394
609
|
|
|
@@ -398,7 +613,7 @@ async function main() {
|
|
|
398
613
|
process.exit(0);
|
|
399
614
|
}
|
|
400
615
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
401
|
-
console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName }));
|
|
616
|
+
console.log(welcomeBox(VERSION, null, { mascot: state.config.mascot, userName: state.config.userName, modo: state.mode }));
|
|
402
617
|
console.log(HELP);
|
|
403
618
|
process.exit(0);
|
|
404
619
|
}
|
|
@@ -417,22 +632,26 @@ async function main() {
|
|
|
417
632
|
} catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
|
|
418
633
|
}
|
|
419
634
|
|
|
420
|
-
|
|
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);
|
|
421
638
|
await runAct(fullPrompt, {
|
|
422
639
|
server: state.config.server,
|
|
423
640
|
model: state.config.model,
|
|
424
641
|
token: state.config.token,
|
|
425
642
|
history: [],
|
|
426
643
|
projectDir: plan.dir,
|
|
427
|
-
projectName: plan.
|
|
644
|
+
projectName: plan.name || state.currentProject,
|
|
428
645
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
429
646
|
});
|
|
430
647
|
process.exit(0);
|
|
431
648
|
}
|
|
432
649
|
|
|
433
650
|
console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null, {
|
|
651
|
+
modo: state.mode,
|
|
434
652
|
mascot: state.config.mascot,
|
|
435
653
|
userName: state.config.userName,
|
|
654
|
+
pastaPadrao: state.workDirOrigem === 'padrao',
|
|
436
655
|
}));
|
|
437
656
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
|
|
438
657
|
rl.prompt();
|
package/lib/act.js
CHANGED
|
@@ -14,6 +14,7 @@ const { c, actionHeader, Spinner, friendlyError } = require('./ui');
|
|
|
14
14
|
const tools = require('./tools');
|
|
15
15
|
const api = require('./api');
|
|
16
16
|
const cfg = require('./config');
|
|
17
|
+
const catalogo = require('./catalogo');
|
|
17
18
|
|
|
18
19
|
const MAX_ITERATIONS = 15;
|
|
19
20
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
@@ -136,9 +137,13 @@ async function runAct(userPrompt, opts) {
|
|
|
136
137
|
ensureWorkDir, profundidade = 0, subagent = false,
|
|
137
138
|
} = opts || {};
|
|
138
139
|
|
|
139
|
-
// A pasta só nasce quando o agente for mesmo
|
|
140
|
-
//
|
|
141
|
-
|
|
140
|
+
// A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
|
|
141
|
+
// Antes ela nascia na primeira tool qualquer — pedir "abre o Chrome" criava
|
|
142
|
+
// um projeto vazio, e ~/primocode enchia de pasta que não era projeto de
|
|
143
|
+
// nada. Quem decide é o catálogo: `escreve: true`.
|
|
144
|
+
let criarPasta = ensureWorkDir || null;
|
|
145
|
+
let pastaAtual = projectDir; // onde as tools agem AGORA
|
|
146
|
+
let temPasta = !criarPasta; // já é a pasta definitiva?
|
|
142
147
|
const falhasPendentes = [];
|
|
143
148
|
let tentativasDeEncerrar = 0;
|
|
144
149
|
|
|
@@ -235,16 +240,21 @@ async function runAct(userPrompt, opts) {
|
|
|
235
240
|
|
|
236
241
|
if (tc.name === 'spawn_agent') {
|
|
237
242
|
const r = await runSubagent(tc.arguments || {}, {
|
|
238
|
-
server, model, token, projectDir, projectName, profundidade,
|
|
243
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade,
|
|
239
244
|
});
|
|
240
245
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
241
246
|
continue;
|
|
242
247
|
}
|
|
243
248
|
|
|
244
|
-
if (
|
|
249
|
+
if (criarPasta && catalogo.escreveArquivo(tc.name)) {
|
|
250
|
+
const nova = criarPasta();
|
|
251
|
+
if (nova) pastaAtual = nova;
|
|
252
|
+
criarPasta = null;
|
|
253
|
+
temPasta = true;
|
|
254
|
+
}
|
|
245
255
|
|
|
246
256
|
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
247
|
-
const result = await tools.execute(tc.name, tc.arguments, { projectDir });
|
|
257
|
+
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
248
258
|
logResult(tc.name, result, profundidade);
|
|
249
259
|
|
|
250
260
|
// Uma escrita bem-sucedida limpa a dívida: o agente se corrigiu.
|
|
@@ -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
|
-
|
|
268
|
-
|
|
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('
|
|
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
|
|
package/lib/catalogo.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O que o PrimoCode sabe fazer — a lista, em um lugar só.
|
|
3
|
+
*
|
|
4
|
+
* Existe porque "o que ele faz" estava espalhado entre o README, o /help, o
|
|
5
|
+
* prompt do servidor e o switch do `tools.js`, e quatro cópias da mesma verdade
|
|
6
|
+
* significam que pelo menos uma está mentindo.
|
|
7
|
+
*
|
|
8
|
+
* O `test/catalogo.test.js` cruza esta lista com as tools que o `tools.execute`
|
|
9
|
+
* realmente atende: item aqui que não existe lá derruba o teste, e tool que
|
|
10
|
+
* existe lá e não está aqui também. A lista não consegue mentir.
|
|
11
|
+
*
|
|
12
|
+
* `escreve: true` marca a ferramenta que cria ou muda arquivo. É o que decide
|
|
13
|
+
* quando nasce uma pasta de projeto — pedir para abrir o Chrome não cria projeto.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const GRUPOS = [
|
|
19
|
+
{
|
|
20
|
+
nome: 'Arquivos',
|
|
21
|
+
resumo: 'ler, criar e mudar arquivo na pasta de trabalho',
|
|
22
|
+
itens: [
|
|
23
|
+
{ tool: 'write_file', escreve: true, faz: 'cria arquivo, ou reescreve inteiro' },
|
|
24
|
+
{ tool: 'edit_file', escreve: true, faz: 'troca um trecho exato, sem reescrever o resto' },
|
|
25
|
+
{ tool: 'multi_edit', escreve: true, faz: 'várias trocas no mesmo arquivo, tudo ou nada' },
|
|
26
|
+
{ tool: 'read_file', escreve: false, faz: 'lê arquivo, inteiro ou por pedaço' },
|
|
27
|
+
{ tool: 'list_dir', escreve: false, faz: 'mostra a árvore de pastas' },
|
|
28
|
+
{ tool: 'glob', escreve: false, faz: 'acha arquivo pelo nome (**/*.js)' },
|
|
29
|
+
{ tool: 'grep', escreve: false, faz: 'acha texto dentro dos arquivos' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
nome: 'Terminal',
|
|
34
|
+
resumo: 'rodar comando na sua máquina',
|
|
35
|
+
itens: [
|
|
36
|
+
{ tool: 'run_shell', escreve: false, faz: 'roda comando e devolve a saída' },
|
|
37
|
+
{ tool: 'run_in_new_terminal', escreve: false, faz: 'abre outro terminal para o que fica de pé (servidor, watch)' },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
nome: 'Navegador',
|
|
42
|
+
resumo: 'controla um navegador de verdade (Chrome, Edge, Brave, Arc…) — clica, digita, lê, fotografa',
|
|
43
|
+
itens: [
|
|
44
|
+
{ tool: 'browser_open', escreve: false, faz: 'abre uma página ou um arquivo seu' },
|
|
45
|
+
{ tool: 'browser_click', escreve: false, faz: 'clica por texto, por seletor ou por coordenada' },
|
|
46
|
+
{ tool: 'browser_type', escreve: false, faz: 'digita num campo' },
|
|
47
|
+
{ tool: 'browser_key', escreve: false, faz: 'aperta tecla (Enter, Tab, Escape)' },
|
|
48
|
+
{ tool: 'browser_read', escreve: false, faz: 'lê o texto da página para conferir o resultado' },
|
|
49
|
+
{ tool: 'browser_screenshot', escreve: true, faz: 'tira foto da página' },
|
|
50
|
+
{ tool: 'browser_scroll', escreve: false, faz: 'rola a página' },
|
|
51
|
+
{ tool: 'browser_wait', escreve: false, faz: 'espera um elemento aparecer' },
|
|
52
|
+
{ tool: 'browser_eval', escreve: false, faz: 'roda JavaScript na página' },
|
|
53
|
+
{ tool: 'open_in_browser', escreve: false, faz: 'abre no seu navegador normal, sem controlar' },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
nome: 'Computador',
|
|
58
|
+
resumo: 'mouse, teclado e tela — age em qualquer programa, não só no navegador',
|
|
59
|
+
itens: [
|
|
60
|
+
{ tool: 'desktop_screenshot', escreve: true, faz: 'tira foto da sua tela' },
|
|
61
|
+
{ tool: 'desktop_click', escreve: false, faz: 'clica na tela' },
|
|
62
|
+
{ tool: 'desktop_move', escreve: false, faz: 'move o mouse' },
|
|
63
|
+
{ tool: 'desktop_drag', escreve: false, faz: 'arrasta de um ponto a outro' },
|
|
64
|
+
{ tool: 'desktop_type', escreve: false, faz: 'digita no programa que está na frente' },
|
|
65
|
+
{ tool: 'desktop_key', escreve: false, faz: 'aperta tecla ou atalho' },
|
|
66
|
+
{ tool: 'desktop_scroll', escreve: false, faz: 'rola a tela' },
|
|
67
|
+
{ tool: 'desktop_size', escreve: false, faz: 'mede a tela, para se orientar antes de clicar' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
nome: 'Organização',
|
|
72
|
+
resumo: 'como ele se organiza numa tarefa grande',
|
|
73
|
+
itens: [
|
|
74
|
+
{ tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou' },
|
|
75
|
+
// Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
|
|
76
|
+
{ tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true },
|
|
77
|
+
{ tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/** Só as que criam ou mudam arquivo. É o gatilho da pasta de projeto. */
|
|
83
|
+
const ESCREVEM = new Set(
|
|
84
|
+
GRUPOS.flatMap((g) => g.itens.filter((i) => i.escreve).map((i) => i.tool)),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const TODAS = GRUPOS.flatMap((g) => g.itens.map((i) => i.tool));
|
|
88
|
+
|
|
89
|
+
/** Esta ferramenta cria ou muda arquivo? */
|
|
90
|
+
function escreveArquivo(tool) {
|
|
91
|
+
return ESCREVEM.has(tool);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { GRUPOS, TODAS, ESCREVEM, escreveArquivo };
|
package/lib/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/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
|
@@ -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
|
+
'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',
|
|
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, pastaPadrao } = opts;
|
|
160
176
|
const art = mascot(style);
|
|
161
177
|
const cols = Math.max(32, Math.min(process.stdout.columns || 100, 120));
|
|
162
178
|
|
|
@@ -166,10 +182,20 @@ function welcomeBox(version, contexto, opts = {}) {
|
|
|
166
182
|
|
|
167
183
|
// As três linhas de identidade — uma por linha do mascote, na ordem em que
|
|
168
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
|
+
|
|
169
195
|
const ident = [
|
|
170
196
|
`${c.bold(c.brand(NOME))} ${c.dim('v' + version)}`,
|
|
171
197
|
saudacao,
|
|
172
|
-
|
|
198
|
+
linhaPasta,
|
|
173
199
|
];
|
|
174
200
|
|
|
175
201
|
const out = [''];
|
|
@@ -200,13 +226,20 @@ function welcomeBox(version, contexto, opts = {}) {
|
|
|
200
226
|
secao('Dicas para começar', TIPS, (i) => `${i + 1}.`);
|
|
201
227
|
secao('Novidades', NEWS, () => ' ·');
|
|
202
228
|
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
229
|
+
// Rodapé: em que modo você está agora, não a lista dos que existem. Em
|
|
230
|
+
// terminal apertado a dica do /help sai antes de empurrar a linha para
|
|
231
|
+
// baixo — cabeçalho que quebra parece defeito.
|
|
232
|
+
// Primária para o modo ativo, secundária quando ele tem poder de escrever:
|
|
233
|
+
// é a mesma leitura de "apurado" × "aviso que não é falha" do sistema.
|
|
234
|
+
const rotulos = {
|
|
235
|
+
auto: `${c.brand('●')} ${c.muted('Auto')}${c.dim(' — conversa vai pro chat, tarefa vai pro agente')}`,
|
|
236
|
+
primocode: `${c.warn('●')} ${c.muted('Primo Code')}${c.dim(' — agente completo, com ferramentas')}`,
|
|
237
|
+
chat: `${c.brandDim('●')} ${c.muted('Chat')}${c.dim(' — conversa pura, sem ferramentas')}`,
|
|
238
|
+
};
|
|
239
|
+
const linhaModo = ' ' + (rotulos[modo] || rotulos.auto);
|
|
208
240
|
out.push('');
|
|
209
|
-
out.push(
|
|
241
|
+
out.push(visibleWidth(linhaModo) <= cols ? linhaModo : ` ${c.ok('●')} ${c.muted(modo || 'auto')}`);
|
|
242
|
+
if (cols >= 44) out.push(` ${c.dim('/help para tudo · /mode troca o modo')}`);
|
|
210
243
|
out.push('');
|
|
211
244
|
return out.join('\n');
|
|
212
245
|
}
|
|
@@ -282,7 +315,7 @@ function actionHeader(name, args) {
|
|
|
282
315
|
return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
283
316
|
}
|
|
284
317
|
|
|
285
|
-
// Transforma erro cru (JSON do
|
|
318
|
+
// Transforma erro cru (JSON do provedor, código HTTP, erro de rede) numa frase
|
|
286
319
|
// humana. O usuário nunca deveria ver um paredão de JSON.
|
|
287
320
|
function friendlyError(raw) {
|
|
288
321
|
const s = String(raw && raw.message ? raw.message : raw || '').trim();
|
|
@@ -298,10 +331,57 @@ function friendlyError(raw) {
|
|
|
298
331
|
}
|
|
299
332
|
} catch {}
|
|
300
333
|
|
|
301
|
-
|
|
302
|
-
|
|
334
|
+
// Falta de saldo: nem bug, nem espera. O provedor diz quanto a conta paga,
|
|
335
|
+
// e o servidor já refaz o pedido com esse teto — se ainda assim chegou aqui,
|
|
336
|
+
// é porque nem o mínimo cabe.
|
|
337
|
+
if (/can only afford|requires more credits|\b402\b/i.test(low)) {
|
|
338
|
+
const q = s.match(/afford\s+(\d+)/i);
|
|
339
|
+
return '💳 A conta do OpenRouter não tem crédito para uma resposta deste tamanho'
|
|
340
|
+
+ (q ? ` (dá para ${q[1]} tokens)` : '')
|
|
341
|
+
+ '. Adicione crédito em openrouter.ai/settings/credits, ou use /model fast'
|
|
342
|
+
+ ' para pedidos menores.';
|
|
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
|
+
}
|
|
353
|
+
// Ordem importa: "grande demais" e "muitos pedidos" chegam os dois como
|
|
354
|
+
// rate_limit_exceeded, e a saída é oposta. Tamanho não se resolve
|
|
355
|
+
// esperando — o pedido vai estourar igual daqui a uma hora.
|
|
356
|
+
const porTamanho = /request too large|tokens per minute|\btpm\b|reduce your message size|\b413\b/i.test(low)
|
|
357
|
+
|| /conversa passou do teto/i.test(low);
|
|
358
|
+
if (porTamanho) {
|
|
359
|
+
const nums = s.match(/Limit\s+(\d+),\s*Requested\s+(\d+)/i);
|
|
360
|
+
if (/conversa passou do teto/i.test(low)) return `📏 ${inner.replace(/\s+/g, ' ').trim()}`;
|
|
361
|
+
return '📏 Este pedido é maior que o teto do plano'
|
|
362
|
+
+ (nums ? ` (${nums[2]} tokens contra um limite de ${nums[1]} por minuto)` : '')
|
|
363
|
+
+ '. Esperar não resolve: use /clear para encurtar a conversa, ou divida o pedido em partes menores.';
|
|
364
|
+
}
|
|
365
|
+
// Cota do dia estourada. Nada a fazer no código, mas o provedor diz quanto
|
|
366
|
+
// falta para liberar — jogar isso fora e escrever "alguns instantes" é
|
|
367
|
+
// mandar o usuário tentar de novo em vão por uma hora e meia.
|
|
368
|
+
if (/tokens per day|\btpd\b/i.test(low)) {
|
|
369
|
+
// A classe [0-9hms.] engole o ponto final da frase do provedor, então tira
|
|
370
|
+
// a pontuação antes de arredondar os milissegundos fora.
|
|
371
|
+
const bruto = (s.match(/try again in ([0-9hms.]+)/i) || [])[1];
|
|
372
|
+
const quando = bruto && bruto.replace(/\.+$/, '').replace(/\.\d+s$/, 's');
|
|
373
|
+
const uso = s.match(/Limit\s+(\d+),\s*Used\s+(\d+)/i);
|
|
374
|
+
return '📅 A cota de tokens do dia acabou'
|
|
375
|
+
+ (uso ? ` (${uso[2]} de ${uso[1]} tokens)` : '')
|
|
376
|
+
+ (quando ? `, libera em ${quando}` : '')
|
|
377
|
+
+ '. Enquanto isso o /mode chat gasta bem menos por pergunta.';
|
|
378
|
+
}
|
|
379
|
+
if (/rate.?limit|too many requests|\b429\b/i.test(low)) {
|
|
380
|
+
return '⏳ Muitos pedidos em pouco tempo. Espere alguns instantes e tente de novo.';
|
|
303
381
|
}
|
|
304
|
-
|
|
382
|
+
// "token" solto não serve de pista: a mensagem de TPM tem a palavra e não
|
|
383
|
+
// tem nada a ver com autenticação. Exige o código ou a frase inteira.
|
|
384
|
+
if (/\b401\b|unauthorized|forbidden|token (inv[aá]lido|ausente|expirado)|invalid token|missing token/i.test(low)) {
|
|
305
385
|
return '🔒 Este servidor pede um token. Configure PRIMOCODE_TOKEN ou "token" em ~/.primocode/config.json.';
|
|
306
386
|
}
|
|
307
387
|
if (/econnrefused|enotfound|etimedout|timeout|socket hang up|network|getaddrinfo|econnreset/i.test(low)) {
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
4
|
-
"description": "PrimoCode — agente de engenharia com IA e cursor próprio.
|
|
3
|
+
"version": "7.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"
|
|
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"
|