primocode 10.5.0 → 10.6.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 +2 -2
- package/bin/primocode.js +117 -24
- package/lib/act.js +141 -9
- package/lib/agents-md.js +140 -0
- package/lib/auditoria.js +122 -0
- package/lib/browser.js +48 -1
- package/lib/catalogo.js +24 -4
- package/lib/comandos-extra.js +2 -0
- package/lib/config.js +32 -4
- package/lib/entrada.js +33 -11
- package/lib/estudio/colecao.js +34 -1
- package/lib/estudio/diretor.js +1 -0
- package/lib/estudio/falantes.js +30 -6
- package/lib/estudio/hibrido.js +144 -8
- package/lib/estudio/index.js +151 -59
- package/lib/estudio/kit/mundo.jsx +1060 -0
- package/lib/estudio/kit/voxel.jsx +315 -70
- package/lib/estudio/kit.js +1 -1
- package/lib/estudio/kokoro.js +18 -4
- package/lib/estudio/margem.js +1 -1
- package/lib/estudio/mundo.js +305 -0
- package/lib/estudio/personagem.js +164 -11
- package/lib/estudio/producao.js +107 -17
- package/lib/estudio/projeto.js +77 -22
- package/lib/leitura.js +176 -0
- package/lib/local/cerebro.json +180 -3
- package/lib/local/entrada.js +61 -4
- package/lib/mcp.js +296 -12
- package/lib/memoria-conversa.js +32 -3
- package/lib/mencoes.js +5 -1
- package/lib/nome-curto.js +126 -0
- package/lib/nome-projeto.js +5 -2
- package/lib/outra-pasta.js +161 -0
- package/lib/perigo.js +34 -2
- package/lib/referencia.js +40 -11
- package/lib/repeticao.js +20 -1
- package/lib/tela.js +1 -1
- package/lib/tools.js +76 -6
- package/lib/travado.js +141 -0
- package/lib/ui.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outra-pasta.js — a pessoa falou de OUTRA pasta: olhar lá, e trocar só com o sim dela.
|
|
3
|
+
*
|
|
4
|
+
* "Quando a IA perceber que eu perguntei sobre algo de outra pasta, ela
|
|
5
|
+
* tem que ir procurar lá. E se ela estiver aberta numa pasta e eu estiver
|
|
6
|
+
* trabalhando em outra, ela muda de pasta — com permissão, claro."
|
|
7
|
+
* (11/10/2026)
|
|
8
|
+
*
|
|
9
|
+
* Duas coisas diferentes, de propósito:
|
|
10
|
+
*
|
|
11
|
+
* · LER lá não pede licença: foi a própria pessoa que citou a pasta. O
|
|
12
|
+
* agente ganha leitura (read_file, list_dir, grep, glob) naquela pasta,
|
|
13
|
+
* sem sair de onde está. Escrever continua só na pasta de trabalho.
|
|
14
|
+
* · TRABALHAR lá é trocar a pasta da sessão — e isso SEMPRE pergunta. Ela
|
|
15
|
+
* muda onde os arquivos nascem, e o "sim" é da pessoa, não da heurística.
|
|
16
|
+
*
|
|
17
|
+
* Antes, citar "pasta loja" trocava sozinho (lib/pasta.js), e um projeto só
|
|
18
|
+
* era achado com a palavra "pasta" na frente. Agora "olha o landing-padaria"
|
|
19
|
+
* também acha — pelo nome dos projetos em ~/primocode e das pastas recentes.
|
|
20
|
+
*
|
|
21
|
+
* `detectar` e `pedirTroca` são puros (quem toca disco e terminal entra por
|
|
22
|
+
* parâmetro), para o teste medir a regra sem disco e sem ninguém digitando.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const pasta = require('./pasta.js');
|
|
31
|
+
|
|
32
|
+
// Nome que é palavra comum: "app", "site", "teste" aparecem em qualquer
|
|
33
|
+
// frase e não querem dizer a pasta de mesmo nome.
|
|
34
|
+
const COMUNS = new Set(['app', 'site', 'teste', 'test', 'projeto', 'project', 'src', 'docs', 'novo',
|
|
35
|
+
'video', 'videos', 'web', 'api', 'lib', 'dist', 'build', 'public', 'assets', 'dados', 'data', 'tmp']);
|
|
36
|
+
|
|
37
|
+
function semAcento(s) { return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase(); }
|
|
38
|
+
|
|
39
|
+
function ehPasta(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* As pastas que a pessoa conhece pelo NOME: os projetos de ~/primocode e as
|
|
43
|
+
* recentes (projeto-ativo.json). Mesmo nome em dois lugares: fica a recente.
|
|
44
|
+
*/
|
|
45
|
+
function conhecidas({ raiz, recentes = [] } = {}) {
|
|
46
|
+
const lista = [];
|
|
47
|
+
const vistos = new Set();
|
|
48
|
+
const incluir = (caminho) => {
|
|
49
|
+
const abs = path.resolve(caminho);
|
|
50
|
+
if (vistos.has(abs) || !ehPasta(abs)) return;
|
|
51
|
+
vistos.add(abs);
|
|
52
|
+
lista.push({ nome: path.basename(abs), caminho: abs });
|
|
53
|
+
};
|
|
54
|
+
for (const r of recentes) incluir(r);
|
|
55
|
+
if (raiz) {
|
|
56
|
+
try {
|
|
57
|
+
for (const d of fs.readdirSync(raiz, { withFileTypes: true })) {
|
|
58
|
+
if (d.isDirectory() && !d.name.startsWith('.')) incluir(path.join(raiz, d.name));
|
|
59
|
+
}
|
|
60
|
+
} catch { /* sem raiz ainda */ }
|
|
61
|
+
}
|
|
62
|
+
return lista;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* O pedido cita uma pasta conhecida pelo nome?
|
|
67
|
+
*
|
|
68
|
+
* Só nome com PELO MENOS DUAS palavras (`landing-padaria`, `jogo_carro`) casa
|
|
69
|
+
* solto na frase — escrito com hífen, sublinhado ou espaço. Nome de uma
|
|
70
|
+
* palavra só ("loja") casaria com "faz uma loja" e trocaria de pasta por
|
|
71
|
+
* acaso; para ele vale a marca explícita do lib/pasta.js ("pasta loja").
|
|
72
|
+
*/
|
|
73
|
+
function porNome(texto, lista) {
|
|
74
|
+
const t = ` ${semAcento(texto).replace(/[^a-z0-9]+/g, ' ').trim()} `;
|
|
75
|
+
const achados = [];
|
|
76
|
+
for (const p of lista || []) {
|
|
77
|
+
const partes = semAcento(p.nome).split(/[^a-z0-9]+/).filter(Boolean);
|
|
78
|
+
if (partes.length < 2 || COMUNS.has(partes.join('-'))) continue;
|
|
79
|
+
if (partes.join('').length < 6) continue;
|
|
80
|
+
if (t.includes(` ${partes.join(' ')} `)) achados.push({ ...p, como: 'nome' });
|
|
81
|
+
}
|
|
82
|
+
// O nome mais comprido primeiro: "jogo-carro-mundo-aberto" ganha de "jogo-carro".
|
|
83
|
+
return achados.sort((a, b) => b.nome.length - a.nome.length);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* As pastas que o pedido cita, fora a atual. Caminho escrito vem primeiro
|
|
88
|
+
* (é o mais certo); depois os nomes conhecidos.
|
|
89
|
+
*
|
|
90
|
+
* `resolver(candidato)` → `{ caminho }` | `{ ambiguo }` | null é o
|
|
91
|
+
* `pasta.resolverPasta` no programa, e um falso no teste.
|
|
92
|
+
*/
|
|
93
|
+
function detectar(texto, { conhecidas: lista = [], atual = '', resolver = null } = {}) {
|
|
94
|
+
const fora = (c) => !atual || path.resolve(c) !== path.resolve(atual);
|
|
95
|
+
const saida = [];
|
|
96
|
+
const candidato = pasta.detectarPasta(texto);
|
|
97
|
+
if (candidato && resolver) {
|
|
98
|
+
const achado = resolver(candidato);
|
|
99
|
+
if (achado && achado.ambiguo) return { ambiguo: achado.ambiguo, candidato };
|
|
100
|
+
if (achado && achado.caminho && fora(achado.caminho)) {
|
|
101
|
+
saida.push({ caminho: path.resolve(achado.caminho), nome: candidato, como: 'caminho' });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const p of porNome(texto, lista)) {
|
|
105
|
+
if (fora(p.caminho) && !saida.some((s) => s.caminho === p.caminho)) saida.push(p);
|
|
106
|
+
}
|
|
107
|
+
return { pastas: saida };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A linha que vai junto do pedido quando a pessoa citou outra pasta e o
|
|
112
|
+
* agente continua onde está. Ela diz as duas portas: ler lá agora, ou pedir
|
|
113
|
+
* para trabalhar lá.
|
|
114
|
+
*/
|
|
115
|
+
function notaDeConsulta(caminho, atual) {
|
|
116
|
+
return `[outra pasta] O usuário citou ${caminho}. Você pode LER e PROCURAR lá sem sair de ${atual}: `
|
|
117
|
+
+ `read_file e list_dir com o caminho absoluto; glob com o padrão absoluto ("${caminho}/**/*.js"); `
|
|
118
|
+
+ `grep com glob absoluto ("${caminho}/**"). `
|
|
119
|
+
+ 'Para TRABALHAR lá (criar ou mudar arquivo), chame mudar_pasta — o usuário confirma antes.';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* O pedido de troca do modelo (tool mudar_pasta). Só troca com o SIM.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} args { caminho, motivo }
|
|
126
|
+
* @param {object} ctx { atual, subagent, trocar, resolver }
|
|
127
|
+
* trocar(info) → Promise<{ ok, caminho, memoria? , error? }>: quem PERGUNTA
|
|
128
|
+
* ao usuário e, com o sim, troca a sessão. Sem ele, ninguém pode dizer sim.
|
|
129
|
+
*/
|
|
130
|
+
async function pedirTroca(args, { atual = '', subagent = false, trocar = null, resolver = null } = {}) {
|
|
131
|
+
const bruto = String((args && (args.caminho || args.path || args.pasta)) || '').trim();
|
|
132
|
+
const motivo = String((args && args.motivo) || '').trim();
|
|
133
|
+
if (!bruto) return { ok: false, error: 'faltou o caminho da pasta (caminho).' };
|
|
134
|
+
if (subagent) return { ok: false, error: 'subagente não troca a pasta da sessão — devolva ao agente principal o que achou.' };
|
|
135
|
+
|
|
136
|
+
const resolve = resolver || ((c) => pasta.resolverPasta(c, { cwd: atual || process.cwd() }));
|
|
137
|
+
const achado = resolve(bruto);
|
|
138
|
+
if (!achado) return { ok: false, error: `não achei a pasta "${bruto}". Confira o caminho (list_dir) ou peça ao usuário.` };
|
|
139
|
+
if (achado.ambiguo) {
|
|
140
|
+
return { ok: false, error: `há mais de uma pasta "${bruto}"; chame de novo com o caminho completo.`,
|
|
141
|
+
opcoes: achado.ambiguo.map((a) => a.caminho) };
|
|
142
|
+
}
|
|
143
|
+
const destino = path.resolve(achado.caminho);
|
|
144
|
+
if (atual && destino === path.resolve(atual)) return { ok: true, caminho: destino, aviso: 'já está nesta pasta.' };
|
|
145
|
+
if (destino === path.resolve(os.homedir()) || destino === path.parse(destino).root) {
|
|
146
|
+
return { ok: false, error: 'a home e a raiz do disco não são pasta de trabalho. Escolha a pasta do projeto.' };
|
|
147
|
+
}
|
|
148
|
+
if (typeof trocar !== 'function') {
|
|
149
|
+
return { ok: false, error: 'trocar de pasta precisa do sim do usuário, e não há ninguém para responder neste modo. '
|
|
150
|
+
+ 'Leia lá se precisar e diga a ele o caminho para usar /dir.' };
|
|
151
|
+
}
|
|
152
|
+
const r = await trocar({ caminho: destino, motivo });
|
|
153
|
+
if (!r || !r.ok) {
|
|
154
|
+
return { ok: false, recusado: true,
|
|
155
|
+
error: (r && r.error) || 'o usuário NÃO quis trocar de pasta. Continue na atual; ler a outra continua liberado.' };
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, caminho: r.caminho || destino, ...(r.memoria ? { memoria: r.memoria } : {}),
|
|
158
|
+
comoUsar: 'a pasta de trabalho agora é esta: caminhos relativos valem a partir dela.' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { conhecidas, porNome, detectar, notaDeConsulta, pedirTroca, COMUNS };
|
package/lib/perigo.js
CHANGED
|
@@ -164,14 +164,39 @@ function analisarTrecho(palavras, estado) {
|
|
|
164
164
|
return null;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/* O LIVRO DE AÇÕES (lib/auditoria.js) mora fora do projeto, e o `rm` nele já
|
|
168
|
+
pedia licença. Mas `> livro`, `sed -i`, `truncate` e `mv` passavam: o
|
|
169
|
+
agente podia apagar ou reescrever o registro do que ele mesmo fez (review
|
|
170
|
+
do #95, item 3). Ler o livro (cat, grep, tail…) segue livre. */
|
|
171
|
+
const LIVRO = path.join(os.homedir(), '.primocode', 'registro');
|
|
172
|
+
const SO_LE = new Set(['cat', 'head', 'tail', 'less', 'more', 'grep', 'rg', 'wc', 'ls', 'jq', 'nl', 'stat', 'file', 'cd']);
|
|
173
|
+
function mexeNoLivro(palavras, cwd) {
|
|
174
|
+
const p = palavras.filter((x) => !PREFIXOS.has(x) && !/^[A-Za-z_]\w*=/.test(x));
|
|
175
|
+
if (!p.length) return false;
|
|
176
|
+
const noLivro = (x) => {
|
|
177
|
+
const alvo = path.resolve(cwd, expandir(String(x).replace(/^\d?>>?/, '')));
|
|
178
|
+
return alvo === LIVRO || dentro(LIVRO, alvo);
|
|
179
|
+
};
|
|
180
|
+
// Redirecionar para dentro do livro é escrever nele, seja qual for o comando.
|
|
181
|
+
for (let i = 0; i < p.length; i++) {
|
|
182
|
+
const redireciona = /^\d?>>?$/.test(p[i]) ? p[i + 1] : /^\d?>>?./.test(p[i]) ? p[i] : /.>>?./.test(p[i]) ? p[i].split(/>>?/).pop() : null;
|
|
183
|
+
if (redireciona && noLivro(redireciona)) return true;
|
|
184
|
+
}
|
|
185
|
+
if (SO_LE.has(path.basename(p[0]))) return false;
|
|
186
|
+
return p.slice(1).some((x) => !/^-/.test(x) && noLivro(x.split(/>>?/)[0]));
|
|
187
|
+
}
|
|
188
|
+
|
|
167
189
|
function analisarComando(comando, cwd, projeto) {
|
|
168
190
|
const s = String(comando || '');
|
|
169
191
|
// Baixar e rodar na hora: o que roda é o que o site mandar naquele segundo.
|
|
170
192
|
if (/\b(curl|wget)\b[^;&]*\|\s*(sudo\s+)?(env\s+)?(ba|z|da|k)?sh\b/.test(s)) {
|
|
171
193
|
return 'baixa um script da internet e roda sem ler';
|
|
172
194
|
}
|
|
195
|
+
// O mesmo que mcp_conectar, pela porta dos fundos: não pula o portão.
|
|
196
|
+
if (/\bprimocode\s+mcp\s+(conectar|connect|add)\b/.test(s)) return 'instala e roda um servidor MCP de terceiros';
|
|
173
197
|
const estado = { cwd, projeto, pulandoFlags: false };
|
|
174
198
|
for (const t of trechos(s)) {
|
|
199
|
+
if (mexeNoLivro(t.palavras, estado.cwd)) return 'mexe no livro de ações do PrimoCode (~/.primocode/registro), o registro do que o agente fez';
|
|
175
200
|
const r = analisarTrecho(t.palavras, estado);
|
|
176
201
|
if (r) return r;
|
|
177
202
|
}
|
|
@@ -189,6 +214,12 @@ function classificar(nome, args, { projectDir } = {}) {
|
|
|
189
214
|
if (COMANDOS.has(nome)) {
|
|
190
215
|
const cwd = a.cwd ? path.resolve(projeto, expandir(a.cwd)) : projeto;
|
|
191
216
|
motivo = analisarComando(a.command, cwd, projeto);
|
|
217
|
+
} else if (nome === 'mcp_conectar') {
|
|
218
|
+
/* Instala e roda código de terceiro (npx/uvx/docker) com as chaves do
|
|
219
|
+
usuário: sempre pergunta, como o curl | sh. O motivo leva o comando,
|
|
220
|
+
então "a sessão inteira" libera AQUELE servidor, não qualquer um. */
|
|
221
|
+
const cmd = [a.comando || a.command, ...(Array.isArray(a.args) ? a.args : [])].filter(Boolean).join(' ');
|
|
222
|
+
motivo = `instala e roda o servidor MCP "${a.nome || '?'}"${cmd ? ` (${cmd.slice(0, 80)})` : ''}`;
|
|
192
223
|
} else if (ESCRITAS.has(nome) && a.path) {
|
|
193
224
|
const alvo = path.resolve(projeto, expandir(a.path));
|
|
194
225
|
const rel = path.relative(projeto, alvo);
|
|
@@ -216,12 +247,13 @@ async function portao(estado, { modo, nome, args, projectDir, subagent = false,
|
|
|
216
247
|
const m = normalizarModo(modo);
|
|
217
248
|
if (m === 'livre') return { ok: true };
|
|
218
249
|
const { perigoso, motivo: motivoPerigo } = classificar(nome, args, { projectDir });
|
|
219
|
-
const tipo = perigoso ? 'perigo' : (m === 'perguntar' && COMANDOS.has(nome)) ? 'comando'
|
|
250
|
+
const tipo = perigoso ? 'perigo' : (m === 'perguntar' && (COMANDOS.has(nome) || nome === 'mcp_usar')) ? 'comando'
|
|
220
251
|
: (m === 'perguntar' && ESCRITAS.has(nome)) ? 'arquivo' : null;
|
|
221
252
|
if (!tipo) return { ok: true };
|
|
222
253
|
|
|
223
254
|
const motivo = perigoso ? motivoPerigo
|
|
224
|
-
: tipo === 'comando' ? `
|
|
255
|
+
: tipo === 'comando' ? (nome === 'mcp_usar' ? `usar ${(args || {}).servidor}.${(args || {}).ferramenta}`
|
|
256
|
+
: `rodar: ${String((args || {}).command || '').slice(0, 80)}`)
|
|
225
257
|
: `escrever ${String((args || {}).path || 'um arquivo')}`;
|
|
226
258
|
const recusa = (texto) => ({ ok: false, motivo, error: texto });
|
|
227
259
|
|
package/lib/referencia.js
CHANGED
|
@@ -28,6 +28,7 @@ const http = require('http');
|
|
|
28
28
|
const net = require('net');
|
|
29
29
|
const tls = require('tls');
|
|
30
30
|
const { URL } = require('url');
|
|
31
|
+
const leitura = require('./leitura');
|
|
31
32
|
|
|
32
33
|
// Achado de segurança: redirect sem checar destino é SSRF — uma fonte de
|
|
33
34
|
// busca (conteúdo de terceiro, não confiável) pode responder com um Location
|
|
@@ -231,6 +232,13 @@ function textoDe(html) {
|
|
|
231
232
|
.trim();
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
/* Uma fonte lida, com o passo da cascata que a leu: o terminal mostra, e o
|
|
236
|
+
modelo sabe se a página veio pela busca simples ou pelo navegador. */
|
|
237
|
+
function fonteLida(cand, trecho, passo, chars) {
|
|
238
|
+
return { titulo: cand.titulo, url: cand.url, nota: cand.nota, trecho,
|
|
239
|
+
leitura: { passo, como: passo === 1 ? 'busca simples' : 'navegador', chars, enviados: trecho.length } };
|
|
240
|
+
}
|
|
241
|
+
|
|
234
242
|
/**
|
|
235
243
|
* ── FONTE ERRADA É PIOR QUE FONTE NENHUMA ────────────────────────────────
|
|
236
244
|
* Pedido real: "pesquise sobre a Paris Group e crie um vídeo para anunciar a
|
|
@@ -665,7 +673,7 @@ async function docsConhecidas(consulta) {
|
|
|
665
673
|
* A pesquisa completa: busca, abre as melhores páginas e devolve o texto
|
|
666
674
|
* delas para o modelo LER. O modelo escreve o código dele com isso na mão.
|
|
667
675
|
*/
|
|
668
|
-
async function pesquisar({ consulta, paginas = 3 }) {
|
|
676
|
+
async function pesquisar({ consulta, paginas = 3, navegador } = {}) {
|
|
669
677
|
const termo = String(consulta || '').trim();
|
|
670
678
|
if (!termo) return { ok: false, error: 'diga o que pesquisar' };
|
|
671
679
|
|
|
@@ -742,6 +750,7 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
742
750
|
// Os chutes de domínio ficam de lado até o fim: só então dá para saber
|
|
743
751
|
// qual deles fala mais do assunto.
|
|
744
752
|
const chutesLidos = [];
|
|
753
|
+
const finas = [];
|
|
745
754
|
// As páginas abrem JUNTAS, não uma depois da outra: dez candidatos a 2 s
|
|
746
755
|
// cada eram 20 s de espera antes de ler a primeira linha. Quem não abriu
|
|
747
756
|
// vira null e é pulado na ordem — a ordem de confiança continua valendo.
|
|
@@ -752,10 +761,17 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
752
761
|
if (lidas.length >= paginas) break;
|
|
753
762
|
const html = paginasAbertas[ic];
|
|
754
763
|
if (!html) continue;
|
|
755
|
-
|
|
764
|
+
// Passo 1 da leitura em cascata (lib/leitura.js): a estrutura sai
|
|
765
|
+
// aqui, local, e o modelo nunca vê o HTML cru.
|
|
766
|
+
const est = leitura.estruturaDe(html, cand.url);
|
|
767
|
+
const texto = est.texto;
|
|
756
768
|
// 400, não 200: uma casca de página que carrega tudo por JavaScript
|
|
757
769
|
// deixa 200-300 caracteres de menu, e isso passava como "fonte lida".
|
|
758
|
-
|
|
770
|
+
// Ela não é descartada: fica pro passo 2 (o navegador), lá embaixo.
|
|
771
|
+
if (texto.length < leitura.MINIMO) {
|
|
772
|
+
if (cand.nota !== 'site provável da empresa') finas.push({ cand, html });
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
759
775
|
if (pareceCodigo(texto)) continue;
|
|
760
776
|
// O chute de domínio precisa PROVAR que acertou. "Paris Group" tem
|
|
761
777
|
// parisgroup.com (um grupo de varejo no exterior) e parisgroup.ai (a
|
|
@@ -778,15 +794,28 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
778
794
|
// Passar não basta: entre dois domínios do MESMO nome, fica o que
|
|
779
795
|
// fala mais do assunto. parisgroup.ai e parisgroup.co os dois
|
|
780
796
|
// "batiam", e o agente recebia as duas empresas como se fossem uma.
|
|
781
|
-
chutesLidos.push({ nota, item:
|
|
797
|
+
chutesLidos.push({ nota, item: fonteLida(cand, leitura.caber(est, MAX_TEXTO).conteudo, 1, texto.length) });
|
|
782
798
|
continue;
|
|
783
799
|
}
|
|
784
|
-
lidas.push(
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
800
|
+
lidas.push(fonteLida(cand, leitura.caber(est, MAX_TEXTO).conteudo, 1, texto.length));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Passo 2: faltou fonte e houve página fina (SPA) — o navegador escondido
|
|
804
|
+
// renderiza o JavaScript. Só duas, e as duas dividem UM teto de 20 s.
|
|
805
|
+
// A URL passa pela mesma trava da busca simples antes de o Chrome abrir:
|
|
806
|
+
// redirect e pedido seguinte continuam travados dentro do htmlEscondido.
|
|
807
|
+
const prazoPasso2 = Date.now() + 20000;
|
|
808
|
+
const abrirNoNavegador = navegador === undefined
|
|
809
|
+
? (u) => require('./browser').htmlEscondido(u) : navegador;
|
|
810
|
+
for (const { cand, html } of finas.slice(0, 2)) {
|
|
811
|
+
if (lidas.length >= paginas || !abrirNoNavegador || Date.now() >= prazoPasso2) break;
|
|
812
|
+
let host = '';
|
|
813
|
+
try { host = new URL(cand.url).hostname; } catch { continue; }
|
|
814
|
+
if (hostBloqueado(host)) continue;
|
|
815
|
+
const resta = prazoPasso2 - Date.now();
|
|
816
|
+
const r = await leitura.ler(cand.url, { html, orcamento: MAX_TEXTO,
|
|
817
|
+
navegador: (u) => Promise.race([abrirNoNavegador(u), new Promise((ok) => setTimeout(ok, resta, null))]) });
|
|
818
|
+
if (r.ok && r.passo === 2 && !pareceCodigo(r.texto)) lidas.push(fonteLida(cand, r.conteudo, 2, r.chars));
|
|
790
819
|
}
|
|
791
820
|
|
|
792
821
|
// O melhor chute entra na frente: quando a empresa tem site, ele é a
|
|
@@ -820,6 +849,6 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
820
849
|
};
|
|
821
850
|
}
|
|
822
851
|
|
|
823
|
-
module.exports = { pesquisar, buscar, stackoverflow, textoDe, buscarWikipedia, buscarRespostaDDG, pegar,
|
|
852
|
+
module.exports = { pesquisar, buscar, stackoverflow, textoDe, buscarWikipedia, buscarRespostaDDG, pegar, hostBloqueado,
|
|
824
853
|
relevante, quantoBate, palavrasDaConsulta, sitesProvaveis, chutesVivos, pareceCodigo,
|
|
825
854
|
destinoBing, destinoYahoo, buscarBing, buscarYahoo, buscarMDN, docsConhecidas, PRAZO_BUSCADOR };
|
package/lib/repeticao.js
CHANGED
|
@@ -142,6 +142,25 @@ function executou(registro, nome, args) {
|
|
|
142
142
|
registro.olhouDesde = false;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* O `ver` sem registrar nada: a chamada SERIA repetição? E a chave dela.
|
|
147
|
+
*
|
|
148
|
+
* Quem usa é o lote de leituras em paralelo (lib/act.js): ele roda antes do
|
|
149
|
+
* laço, e o que o laço depois recusasse como repetido já teria rodado — e o
|
|
150
|
+
* livro (lib/auditoria.js) anotava "recusada" para algo que aconteceu.
|
|
151
|
+
*/
|
|
152
|
+
function espiar(registro, nome, args) {
|
|
153
|
+
const busca = consultaDe(nome, args);
|
|
154
|
+
const foraDaEpoca = busca !== null || REFERENCIAS.has(nome);
|
|
155
|
+
const base = assinatura(nome, args);
|
|
156
|
+
const epoca = (base === registro.ultimaMudanca && !registro.olhouDesde)
|
|
157
|
+
? registro.epocaDaMudanca : (registro.epoca || 0);
|
|
158
|
+
const chave = base + (foraDaEpoca ? '' : `@${epoca}`);
|
|
159
|
+
const repetida = registro.vistos.has(chave)
|
|
160
|
+
|| (!!busca && registro.buscas.some((b) => semelhanca(b.texto, busca) >= LIMITE_SEMELHANCA));
|
|
161
|
+
return { repetida, chave };
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
/**
|
|
146
165
|
* Registra a chamada e diz se ela é repetição.
|
|
147
166
|
*
|
|
@@ -251,5 +270,5 @@ function referenciasNaConversa(messages) {
|
|
|
251
270
|
module.exports = {
|
|
252
271
|
REFERENCIAS, LIMITE_SEMELHANCA,
|
|
253
272
|
normalizar, palavras, semelhanca, assinatura, consultaDe,
|
|
254
|
-
novo, ver, executou, observa, muda, chamadasPorId, referenciasNaConversa,
|
|
273
|
+
novo, ver, espiar, executou, observa, muda, chamadasPorId, referenciasNaConversa,
|
|
255
274
|
};
|
package/lib/tela.js
CHANGED
|
@@ -221,7 +221,7 @@ function quadro(e) {
|
|
|
221
221
|
conversa.length = altoConversa;
|
|
222
222
|
}
|
|
223
223
|
if (rolagem > 0) {
|
|
224
|
-
conversa[0] = ' ' + c.dim(`↑ ${rolagem} linha${rolagem > 1 ? 's' : ''} abaixo · PgDn
|
|
224
|
+
conversa[0] = ' ' + c.dim(`↑ ${rolagem} linha${rolagem > 1 ? 's' : ''} abaixo · PgDn para voltar`);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
const esquerda = [...conversa, ...caixa.linhas].map((l) => caber(l, mainW));
|
package/lib/tools.js
CHANGED
|
@@ -94,6 +94,59 @@ function resolveInProject(rawPath, projectDir) {
|
|
|
94
94
|
return target;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/* ── LER EM OUTRA PASTA ──────────────────────────────────────────────────
|
|
98
|
+
* A pessoa citou outra pasta ("olha como fiz no landing-padaria"): o agente
|
|
99
|
+
* pode LER e PROCURAR lá, sem trocar a pasta de trabalho (lib/outra-pasta.js).
|
|
100
|
+
* Só leitura, e só nas pastas que a própria pessoa citou nesta sessão —
|
|
101
|
+
* escrever continua preso na pasta de trabalho pelo resolveInProject. */
|
|
102
|
+
const PASTAS_DE_CONSULTA = new Set();
|
|
103
|
+
|
|
104
|
+
function liberarLeitura(dir) {
|
|
105
|
+
if (!dir) return;
|
|
106
|
+
try { if (fs.statSync(dir).isDirectory()) PASTAS_DE_CONSULTA.add(path.resolve(dir)); } catch { /* não existe */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function pastasDeConsulta() { return [...PASTAS_DE_CONSULTA]; }
|
|
110
|
+
function esquecerConsultas() { PASTAS_DE_CONSULTA.clear(); }
|
|
111
|
+
|
|
112
|
+
/** Caminho para LER: dentro do projeto, ou dentro de uma pasta citada. */
|
|
113
|
+
function resolverParaLer(rawPath, projectDir) {
|
|
114
|
+
try {
|
|
115
|
+
return resolveInProject(rawPath, projectDir);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
const bruto = String(rawPath || '').replace(/^~(?=$|[/\\])/, os.homedir());
|
|
118
|
+
if (path.isAbsolute(bruto)) {
|
|
119
|
+
const alvo = path.resolve(bruto);
|
|
120
|
+
for (const raiz of PASTAS_DE_CONSULTA) {
|
|
121
|
+
const rel = path.relative(raiz, alvo);
|
|
122
|
+
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) return alvo;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`${e.message}. Ler fora da pasta de trabalho só vale nas pastas que o usuário citou; `
|
|
126
|
+
+ 'para trabalhar em outra pasta, use mudar_pasta.');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Onde procurar (glob/grep): a pasta de trabalho, ou a pasta citada.
|
|
132
|
+
*
|
|
133
|
+
* O schema do servidor não ganhou campo novo (cada token do núcleo conta no
|
|
134
|
+
* teto do minuto): a outra pasta vem no PADRÃO absoluto —
|
|
135
|
+
* "/Users/x/site/src/<estrela>.js" vira a raiz "/Users/x/site/src" e o
|
|
136
|
+
* padrão "<estrela>.js". `pasta` explícito também vale, para quem mandar.
|
|
137
|
+
*/
|
|
138
|
+
function raizDaBusca(padrao, pastaPedida, projectDir) {
|
|
139
|
+
if (pastaPedida) return { raiz: resolverParaLer(pastaPedida, projectDir), padrao, outra: true };
|
|
140
|
+
const bruto = String(padrao || '').replace(/^~(?=$|[/\\])/, os.homedir());
|
|
141
|
+
if (!padrao || !path.isAbsolute(bruto)) return { raiz: projectDir, padrao, outra: false };
|
|
142
|
+
const partes = bruto.split(/[/\\]/);
|
|
143
|
+
const i = partes.findIndex((p) => /[*?{[]/.test(p));
|
|
144
|
+
const fixo = (i === -1 ? partes : partes.slice(0, i)).join('/') || '/';
|
|
145
|
+
const resto = i === -1 ? '**/*' : partes.slice(i).join('/');
|
|
146
|
+
const raiz = resolverParaLer(fixo, projectDir);
|
|
147
|
+
return { raiz, padrao: resto, outra: path.resolve(raiz) !== path.resolve(projectDir) };
|
|
148
|
+
}
|
|
149
|
+
|
|
97
150
|
function treeDir(dir, maxDepth = 2, currentDepth = 0, prefix = '') {
|
|
98
151
|
if (currentDepth > maxDepth) return '';
|
|
99
152
|
let result = '';
|
|
@@ -411,7 +464,7 @@ function editFile({ path: rawPath, old_string, new_string, replace_all }, projec
|
|
|
411
464
|
}
|
|
412
465
|
|
|
413
466
|
function readFile({ path: rawPath, offset, limit }, projectDir) {
|
|
414
|
-
const filePath =
|
|
467
|
+
const filePath = resolverParaLer(rawPath, projectDir);
|
|
415
468
|
const bruto = fs.readFileSync(filePath, 'utf-8');
|
|
416
469
|
const todas = bruto.split('\n');
|
|
417
470
|
|
|
@@ -578,13 +631,20 @@ async function despachar(name, args, ctx) {
|
|
|
578
631
|
case 'read_file':
|
|
579
632
|
return readFile(args, projectDir);
|
|
580
633
|
case 'list_dir':
|
|
581
|
-
return { ok: true, tree: treeDir(
|
|
634
|
+
return { ok: true, tree: treeDir(resolverParaLer(args.path, projectDir), args.depth || 2) };
|
|
635
|
+
/* `pasta` (opcional): procurar numa pasta que o usuário citou, sem
|
|
636
|
+
trocar a de trabalho. Os caminhos voltam relativos a ELA, e a
|
|
637
|
+
resposta diz qual é. */
|
|
582
638
|
case 'glob': {
|
|
583
|
-
const
|
|
584
|
-
|
|
639
|
+
const b = raizDaBusca(args.pattern || '**/*', args.pasta, projectDir);
|
|
640
|
+
const arquivos = globFiles(b.padrao || '**/*', b.raiz);
|
|
641
|
+
return { ok: true, total: arquivos.length, arquivos: arquivos.slice(0, 200), ...(b.outra ? { pasta: b.raiz } : {}) };
|
|
642
|
+
}
|
|
643
|
+
case 'grep': {
|
|
644
|
+
const b = raizDaBusca(args.glob, args.pasta, projectDir);
|
|
645
|
+
const r = grepFiles({ ...args, glob: b.padrao }, b.raiz);
|
|
646
|
+
return b.outra && r && r.ok !== false ? { ...r, pasta: b.raiz } : r;
|
|
585
647
|
}
|
|
586
|
-
case 'grep':
|
|
587
|
-
return grepFiles(args, projectDir);
|
|
588
648
|
case 'run_shell':
|
|
589
649
|
return runShell(args.command, args.timeout_ms || 60000, projectDir, ambienteLimpo());
|
|
590
650
|
case 'run_in_new_terminal':
|
|
@@ -630,6 +690,15 @@ async function despachar(name, args, ctx) {
|
|
|
630
690
|
return { ok: true, gravado: entry };
|
|
631
691
|
}
|
|
632
692
|
|
|
693
|
+
// ── MCP: o serviço pela API dele, antes do mouse (lib/mcp.js) ──
|
|
694
|
+
case 'mcp_procurar':
|
|
695
|
+
return require('./mcp.js').procurar(args.busca || args.query || args.servico || '');
|
|
696
|
+
// Roda código de terceiro: o portão (lib/perigo.js) pergunta antes de chegar aqui.
|
|
697
|
+
case 'mcp_conectar':
|
|
698
|
+
return require('./mcp.js').conectarNovo({ nome: args.nome, command: args.comando || args.command, args: args.args, env: args.env });
|
|
699
|
+
case 'mcp_usar':
|
|
700
|
+
return require('./mcp.js').usar(args.servidor, args.ferramenta, args.argumentos || args.arguments || {}, projectDir);
|
|
701
|
+
|
|
633
702
|
// ── Navegador controlado (CDP) — o agente age numa página real ──
|
|
634
703
|
/* O navegador fala em REFS (lib/snapshot.js): abrir e clicar já
|
|
635
704
|
devolvem o retrato da página com "@e3 [button] Entrar", e o
|
|
@@ -882,4 +951,5 @@ module.exports = {
|
|
|
882
951
|
fmtBytes, treeDir, runShell, ambienteLimpo, openInBrowser, runInNewTerminal,
|
|
883
952
|
execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
|
|
884
953
|
multiEdit, writeTodos, getTodos,
|
|
954
|
+
liberarLeitura, pastasDeConsulta, esquecerConsultas, resolverParaLer,
|
|
885
955
|
};
|
package/lib/travado.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* travado.js — o agente que anda em círculo para antes de comer a cota.
|
|
3
|
+
*
|
|
4
|
+
* ── O QUE ACONTECEU DE VERDADE ───────────────────────────────────────────
|
|
5
|
+
* 01/10: um subagente passou mais de uma hora relendo o mesmo arquivo, em
|
|
6
|
+
* fatias que se sobrepunham, até acabar a cota grátis. O freio de repetição
|
|
7
|
+
* (repeticao.js) só pega a chamada IDÊNTICA — a fatia 1-200 e a 150-350 do
|
|
8
|
+
* mesmo app.js passavam como coisas novas. O único freio era o teto de 20
|
|
9
|
+
* voltas, e 20 voltas de leitura são muita cota.
|
|
10
|
+
*
|
|
11
|
+
* ── A REGRA (do foreman do thruwire: perguntas fechadas, limite em número) ─
|
|
12
|
+
* 1. a mesma fatia (ou uma que se sobrepõe) do mesmo arquivo, lida
|
|
13
|
+
* LIMITE_RELEITURA vezes sem escrita no meio? → travado
|
|
14
|
+
* 2. a mesma ferramenta com os mesmos argumentos LIMITE_MESMA_CHAMADA
|
|
15
|
+
* vezes sem escrita no meio? → travado
|
|
16
|
+
* 3. VOLTAS_SEM_NOVIDADE voltas seguidas sem escrever nada e sem nenhuma
|
|
17
|
+
* chamada que ainda não tinha sido feita? → travado
|
|
18
|
+
* Na primeira vez, um empurrão curto (o que está repetindo, o que fazer). Se
|
|
19
|
+
* travar de novo depois do empurrão, PARA e diz por quê.
|
|
20
|
+
*
|
|
21
|
+
* Puro e determinístico: sem I/O, sem relógio. O teste mede a regra inteira.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const LIMITE_RELEITURA = 3;
|
|
29
|
+
const LIMITE_MESMA_CHAMADA = 4;
|
|
30
|
+
const VOLTAS_SEM_NOVIDADE = 6;
|
|
31
|
+
const ESCRITAS = new Set(['write_file', 'edit_file', 'multi_edit']);
|
|
32
|
+
|
|
33
|
+
function novo() {
|
|
34
|
+
return { leituras: new Map(), chamadas: new Map(), vistas: new Set(), novidade: false, voltasSemNovidade: 0, avisos: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* A faixa que a leitura cobre. Sem offset nem limit, o read_file devolve o
|
|
38
|
+
arquivo inteiro. Com offset e sem limit, devolve LEITURA_PADRAO linhas a
|
|
39
|
+
partir dele (lib/tools.js) — e não "daqui até o fim": era assim, e ler as
|
|
40
|
+
linhas 1, 201 e 401 em sequência, que é o que a dica da read_file manda
|
|
41
|
+
fazer, contava como reler a mesma parte (review do #95, item 1). */
|
|
42
|
+
const LEITURA_PADRAO = 200;
|
|
43
|
+
function faixa(args) {
|
|
44
|
+
const a = args || {};
|
|
45
|
+
if (a.offset == null && a.limit == null) return [1, Infinity];
|
|
46
|
+
const ini = Math.max(1, Number(a.offset) || 1);
|
|
47
|
+
const lim = Number(a.limit) > 0 ? Number(a.limit) : LEITURA_PADRAO;
|
|
48
|
+
return [ini, ini + lim - 1];
|
|
49
|
+
}
|
|
50
|
+
const sobrepoe = (a, b) => a[0] <= b[1] && b[0] <= a[1];
|
|
51
|
+
|
|
52
|
+
/* LER PELO SHELL é ler. O agente que leva o empurrão por reler o app.js
|
|
53
|
+
foge para `cat app.js` ou `sed -n '1,200p' app.js` — e o freio só olhava a
|
|
54
|
+
read_file (item 2). O comando de leitura simples vira a leitura que ele é. */
|
|
55
|
+
function leituraDoShell(comando) {
|
|
56
|
+
const s = String(comando || '').trim();
|
|
57
|
+
const m = /^(?:cd\s+("[^"]+"|'[^']+'|\S+)\s*&&\s*)?(cat|nl|less|more|head|tail|sed)((?:\s+(?:-n\s*)?(?:'[^']*'|"[^"]*"|-\S+|\d+))*)\s+("[^"]+"|'[^']+'|[^\s|;&><'"]+)\s*$/.exec(s);
|
|
58
|
+
if (!m) return null;
|
|
59
|
+
const tira = (x) => String(x || '').replace(/^["']|["']$/g, '');
|
|
60
|
+
const [, cd, cmd, opcoes] = m;
|
|
61
|
+
const arquivo = cd ? path.join(tira(cd), tira(m[4])) : tira(m[4]);
|
|
62
|
+
if (cmd === 'sed') {
|
|
63
|
+
const r = /-n\s*['"]?(\d+)(?:,(\d+))?p['"]?/.exec(opcoes);
|
|
64
|
+
if (!r) return null; // sed que não é "mostra as linhas A a B"
|
|
65
|
+
const ini = Number(r[1]);
|
|
66
|
+
return { path: arquivo, offset: ini, limit: (r[2] ? Number(r[2]) : ini) - ini + 1 };
|
|
67
|
+
}
|
|
68
|
+
if (cmd === 'head') {
|
|
69
|
+
const n = /(\d+)/.exec(opcoes || '');
|
|
70
|
+
return { path: arquivo, offset: 1, limit: n ? Number(n[1]) : 10 };
|
|
71
|
+
}
|
|
72
|
+
return { path: arquivo }; // cat, tail e companhia: o arquivo todo
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* O mesmo arquivo com outro nome é o mesmo arquivo: `app.js`, `./app.js`,
|
|
76
|
+
`/projeto/app.js` e `src/../app.js` (item 2). Sem a pasta, normaliza só o
|
|
77
|
+
texto. Puro: path.resolve não abre nada. */
|
|
78
|
+
function caminhoDe(p, pasta) {
|
|
79
|
+
return pasta ? path.resolve(pasta, String(p)) : path.normalize(String(p));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Olha UMA chamada, antes de ela rodar. Devolve o motivo se travou, ou null. */
|
|
83
|
+
function observar(estado, nome, args, pasta) {
|
|
84
|
+
const a = args || {};
|
|
85
|
+
const chave = `${nome}:${JSON.stringify(a)}`;
|
|
86
|
+
if (ESCRITAS.has(nome)) {
|
|
87
|
+
estado.leituras.clear();
|
|
88
|
+
estado.chamadas.clear();
|
|
89
|
+
estado.novidade = true;
|
|
90
|
+
}
|
|
91
|
+
if (!estado.vistas.has(chave)) { estado.vistas.add(chave); estado.novidade = true; }
|
|
92
|
+
const leitura = nome === 'read_file' ? a : nome === 'run_shell' ? leituraDoShell(a.command) : null;
|
|
93
|
+
if (leitura && leitura.path) {
|
|
94
|
+
const caminho = caminhoDe(leitura.path, pasta);
|
|
95
|
+
const f = faixa(leitura);
|
|
96
|
+
const antes = estado.leituras.get(caminho) || [];
|
|
97
|
+
antes.push(f);
|
|
98
|
+
estado.leituras.set(caminho, antes);
|
|
99
|
+
const vezes = antes.filter((g) => sobrepoe(f, g)).length;
|
|
100
|
+
if (vezes >= LIMITE_RELEITURA) {
|
|
101
|
+
const nomeCurto = pasta ? path.relative(pasta, caminho) || caminho : caminho;
|
|
102
|
+
return `leu ${nomeCurto} ${vezes} vezes (a mesma parte, ou partes que se sobrepõem) sem mudar nada no meio`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const n = (estado.chamadas.get(chave) || 0) + 1;
|
|
106
|
+
estado.chamadas.set(chave, n);
|
|
107
|
+
if (n >= LIMITE_MESMA_CHAMADA) return `chamou ${nome} com os mesmos argumentos ${n} vezes`;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Fecha a volta. Devolve o motivo se já são voltas demais sem nada novo. */
|
|
112
|
+
function fimDaVolta(estado) {
|
|
113
|
+
estado.voltasSemNovidade = estado.novidade ? 0 : estado.voltasSemNovidade + 1;
|
|
114
|
+
estado.novidade = false;
|
|
115
|
+
if (estado.voltasSemNovidade >= VOLTAS_SEM_NOVIDADE) {
|
|
116
|
+
return `${estado.voltasSemNovidade} voltas seguidas sem escrever nada e sem buscar nada novo`;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Travou: a primeira vez empurra; a segunda para. */
|
|
122
|
+
function decidir(estado, motivo) {
|
|
123
|
+
if (!motivo) return null;
|
|
124
|
+
estado.avisos++;
|
|
125
|
+
if (estado.avisos === 1) {
|
|
126
|
+
estado.voltasSemNovidade = 0;
|
|
127
|
+
return {
|
|
128
|
+
acao: 'empurrar',
|
|
129
|
+
texto: `VOCÊ ESTÁ ANDANDO EM CÍRCULO: ${motivo}. O que você já leu está na conversa. `
|
|
130
|
+
+ 'Pare de reler. Faça AGORA o próximo passo concreto (edit_file, write_file, rodar o teste) '
|
|
131
|
+
+ 'ou chame finish dizendo o que conseguiu e o que falta. Mais uma volta assim e eu paro você.',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
acao: 'parar',
|
|
136
|
+
texto: `Parei o agente: ele travou (${motivo}), mesmo depois de avisado. `
|
|
137
|
+
+ 'Continuar só gastaria cota. O que já foi feito ficou feito — refaça o pedido com um passo mais específico.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { LIMITE_RELEITURA, LIMITE_MESMA_CHAMADA, VOLTAS_SEM_NOVIDADE, LEITURA_PADRAO, novo, faixa, leituraDoShell, observar, fimDaVolta, decidir };
|
package/lib/ui.js
CHANGED
|
@@ -558,6 +558,7 @@ const ROTULOS = {
|
|
|
558
558
|
desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
|
|
559
559
|
estudio_midia:'Estúdio·mídia', construir_app:'Construir',
|
|
560
560
|
remember_general:'Lembrar',
|
|
561
|
+
mcp_procurar:'MCP·procurar', mcp_conectar:'MCP·conectar', mcp_usar:'MCP',
|
|
561
562
|
};
|
|
562
563
|
|
|
563
564
|
/* A linha da AÇÃO: `●` na cor da marca e o rótulo em negrito. O resultado
|