terminal-smart-cli 0.39.0 → 0.40.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/bin/ts.js +104 -31
- package/lib/agent.js +10 -0
- package/lib/meta.js +101 -12
- package/lib/tools.js +133 -5
- package/package.json +1 -1
package/bin/ts.js
CHANGED
|
@@ -1149,7 +1149,7 @@ async function metaCmd() {
|
|
|
1149
1149
|
const parts = [];
|
|
1150
1150
|
for (let i = mi + 1; i < rawArgs.length; i++) {
|
|
1151
1151
|
const a = rawArgs[i];
|
|
1152
|
-
if (a.startsWith('-')) { if (['--budget', '--rodadas', '--rounds', '--modelo', '--model', '--pensador', '--thinker', '--maxmin', '--designer', '--design', '--olho', '--eye', '--mockup', '--arquivo', '--file', '-f'].includes(a)) i++; continue; }
|
|
1152
|
+
if (a.startsWith('-')) { if (['--budget', '--rodadas', '--rounds', '--modelo', '--model', '--pensador', '--thinker', '--maxmin', '--designer', '--design', '--olho', '--eye', '--mockup', '--arquivo', '--file', '-f', '--budget-total', '--max-janelas', '--max-windows', '--max-horas', '--intervalo'].includes(a)) i++; continue; }
|
|
1153
1153
|
parts.push(a);
|
|
1154
1154
|
}
|
|
1155
1155
|
// objetivo pode vir de um ARQUIVO (--arquivo/-f caminho.txt) — evita a dor de passar
|
|
@@ -1170,6 +1170,15 @@ async function metaCmd() {
|
|
|
1170
1170
|
const visualLadder = !FLAGS.has('--sem-escada') && !FLAGS.has('--barato'); // escada visual: fix visual escala pro modelo forte (caro). --sem-escada/--barato deixa no executor barato
|
|
1171
1171
|
const mockup = flagStr('--mockup'); // imagem de referência (ex: mockup do Google AI Studio) — designer projeta a partir dela e o olho cobra fidelidade
|
|
1172
1172
|
const arch = FLAGS.has('--sem-arch') ? false : (FLAGS.has('--arch') || FLAGS.has('--arquitetura') ? true : 'auto'); // contrato de arquitetura antes de codar (auto = liga em app complexo)
|
|
1173
|
+
// ── MODO NOTURNO (auto-continuação por orçamento): retoma sozinho quando o teto
|
|
1174
|
+
// de crédito da janela estoura, até a missão terminar OU bater um teto TOTAL. ──
|
|
1175
|
+
const noturno = FLAGS.has('--noturno') || FLAGS.has('--auto');
|
|
1176
|
+
const budgetTotal = flagNum('--budget-total', 0); // teto TOTAL de créditos (0 = sem teto)
|
|
1177
|
+
const maxJanelas = flagNum('--max-janelas', flagNum('--max-windows', 0)); // nº máx. de janelas (0 = sem teto)
|
|
1178
|
+
const maxHoras = flagNum('--max-horas', 0); // teto de horas de parede (0 = sem teto)
|
|
1179
|
+
const intervaloMin = flagNum('--intervalo', 5); // min de espera entre janelas ao aguardar crédito
|
|
1180
|
+
// Anti-loop-infinito: se ninguém definiu teto, cai num limite de janelas seguro.
|
|
1181
|
+
const maxJanelasEff = maxJanelas || ((budgetTotal || maxHoras) ? 0 : 20);
|
|
1173
1182
|
|
|
1174
1183
|
const existing = metaMod.load(dir);
|
|
1175
1184
|
if (FLAGS.has('--status')) {
|
|
@@ -1190,40 +1199,95 @@ async function metaCmd() {
|
|
|
1190
1199
|
const d = st0.checklist.filter(i => i.passes).length;
|
|
1191
1200
|
console.log(' ' + C.dim(T.meta_resuming(d, st0.checklist.length)) + '\n');
|
|
1192
1201
|
}
|
|
1193
|
-
const sp = ui.spinner(T.meta_planning).start();
|
|
1194
1202
|
const t0 = Date.now();
|
|
1195
1203
|
if (forcedModel) console.log(' ' + C.dim('executor fixo: ') + C.cyan(forcedModel) + '\n');
|
|
1196
1204
|
console.log(' ' + C.dim('pensador (escala no erro): ') + C.indigo(thinker) + (maxMinutes ? C.dim(' · limite ' + maxMinutes + ' min') : '') + '\n');
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1205
|
+
if (noturno) {
|
|
1206
|
+
const lim = [];
|
|
1207
|
+
if (budgetTotal) lim.push(`${budgetTotal} créditos totais`);
|
|
1208
|
+
if (maxJanelasEff) lim.push(`${maxJanelasEff} janela(s)`);
|
|
1209
|
+
if (maxHoras) lim.push(`${maxHoras}h`);
|
|
1210
|
+
console.log(' ' + C.warn('🌙') + ' ' + C.dim('modo noturno LIGADO — retomo sozinho ao esgotar crédito' + (lim.length ? ' · teto: ' + lim.join(', ') : '') + ` · intervalo ${intervaloMin} min`) + '\n');
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// ── LOOP DE JANELAS (modo noturno) ─────────────────────────────────────────
|
|
1214
|
+
// Sem --noturno, o for roda UMA vez e sai (comportamento idêntico ao de hoje).
|
|
1215
|
+
// Com --noturno, quando a run PAUSA por falta de crédito, esperamos o intervalo
|
|
1216
|
+
// e RE-INVOCAMOS run() — que retoma sozinho do .ts-meta.json — até concluir ou
|
|
1217
|
+
// bater um teto (créditos totais / janelas / horas / travamento sem progresso).
|
|
1218
|
+
const RETOMAVEL = new Set(['no_credits', 'budget', 'connection']); // pausas que valem re-tentar
|
|
1219
|
+
const _sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
1220
|
+
let st = null, janela = 0, prevPend = null, semProgresso = 0;
|
|
1221
|
+
const loopStart = Date.now();
|
|
1222
|
+
for (;;) {
|
|
1223
|
+
janela++;
|
|
1224
|
+
const sp = ui.spinner(T.meta_planning).start();
|
|
1225
|
+
const goalArg = janela === 1 ? (goal || null) : null; // janelas seguintes só retomam
|
|
1226
|
+
try {
|
|
1227
|
+
st = await metaMod.run(goalArg, {
|
|
1228
|
+
token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch,
|
|
1229
|
+
onAlert: ({ type, text }) => {
|
|
1230
|
+
sp.stop();
|
|
1231
|
+
const ic = type === 'human' ? C.warn('🙋') : type === 'escalate' ? C.indigo('🧠') : type === 'stagnated' ? C.err('🛑') : type === 'design' ? C.cyan('🎨') : type === 'retry' || type === 'conn' ? C.warn('📡') : C.warn('⏱');
|
|
1232
|
+
console.log('\n ' + ic + ' ' + C.bold(String(text).split('\n')[0]));
|
|
1233
|
+
const rest = String(text).split('\n').slice(1).filter(Boolean);
|
|
1234
|
+
for (const l of rest) console.log(' ' + C.dim(l));
|
|
1235
|
+
console.log('');
|
|
1236
|
+
metaMod.notify(token, text).catch(() => {}); // avisa no Telegram também
|
|
1237
|
+
sp.start();
|
|
1238
|
+
},
|
|
1239
|
+
onChecklist: (itens) => { sp.stop(); console.log(_metaChecklistBox(itens) + '\n'); sp.start(); },
|
|
1240
|
+
onRound: ({ n, item, attempt }) => { sp.stop(); console.log(' ' + C.indigo('◆') + ' ' + C.bold(T.meta_round(n, item.slice(0, 70), attempt))); sp.start(); },
|
|
1241
|
+
onThinking: () => sp.text(T.agent_thinking),
|
|
1242
|
+
onStep: ({ name, detail, blocked }) => {
|
|
1243
|
+
sp.stop();
|
|
1244
|
+
console.log(' ' + (blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙')) + ' ' + name + (detail ? C.dim(' · ' + detail) : ''));
|
|
1245
|
+
sp.start();
|
|
1246
|
+
},
|
|
1247
|
+
askApprove: async (cmd) => { sp.stop(); const a = String(await ui.ask(C.err('▲ ') + T.agent_approve(C.bold(cmd)))).trim().toLowerCase(); sp.start(); return ['s', 'sim', 'y', 'yes'].includes(a); },
|
|
1248
|
+
onRemote: ({ ttl }) => { sp.stop(); console.log(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120))); sp.start(); },
|
|
1249
|
+
onRoundDone: ({ checklist, spent }) => {
|
|
1250
|
+
sp.stop();
|
|
1251
|
+
const d = checklist.filter(i => i.passes).length;
|
|
1252
|
+
console.log(' ' + C.dim(T.meta_progress(d, checklist.length, spent, budget)) + '\n');
|
|
1253
|
+
sp.text(T.meta_marking).start();
|
|
1254
|
+
},
|
|
1255
|
+
});
|
|
1256
|
+
} catch (e) {
|
|
1220
1257
|
sp.stop();
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1258
|
+
// run() pode ESTOURAR o teto durante as fases pré-rodada (viabilidade/design/arquitetura),
|
|
1259
|
+
// ANTES de salvar o estado como "paused". Tratamos igual a uma pausa por crédito.
|
|
1260
|
+
if (e && e.code === 'no_credits') {
|
|
1261
|
+
const cur = metaMod.load(dir);
|
|
1262
|
+
st = cur ? Object.assign(cur, { status: 'paused', pause_reason: 'no_credits' })
|
|
1263
|
+
: { status: 'paused', pause_reason: 'no_credits', checklist: [], creditsSpent: 0, budget };
|
|
1264
|
+
} else { throw e; }
|
|
1265
|
+
}
|
|
1266
|
+
sp.stop();
|
|
1267
|
+
if (!st) break;
|
|
1268
|
+
if (st.status === 'done') break; // missão concluída
|
|
1269
|
+
if (!noturno) break; // caminho default: uma janela só
|
|
1270
|
+
if (!RETOMAVEL.has(st.pause_reason)) break; // awaiting_human / stagnated / timeout / max_rounds → para e avisa
|
|
1271
|
+
|
|
1272
|
+
// Guarda anti-giro-em-falso: 2 janelas seguidas sem reduzir os pendentes = para.
|
|
1273
|
+
const pend = (st.checklist || []).filter(i => !i.passes && !i.blocked).length;
|
|
1274
|
+
if (prevPend !== null && pend >= prevPend) semProgresso++; else semProgresso = 0;
|
|
1275
|
+
prevPend = pend;
|
|
1276
|
+
if (semProgresso >= 2) { st._noturnoStop = 'stall'; break; }
|
|
1277
|
+
|
|
1278
|
+
// Tetos absolutos (nunca roda pra sempre).
|
|
1279
|
+
const horas = (Date.now() - loopStart) / 3600000;
|
|
1280
|
+
if (budgetTotal > 0 && (st.creditsSpent || 0) >= budgetTotal) { st._noturnoStop = 'budget-total'; break; }
|
|
1281
|
+
if (maxJanelasEff > 0 && janela >= maxJanelasEff) { st._noturnoStop = 'janelas'; break; }
|
|
1282
|
+
if (maxHoras > 0 && horas >= maxHoras) { st._noturnoStop = 'horas'; break; }
|
|
1283
|
+
|
|
1284
|
+
// Progresso entre janelas + espera o crédito voltar.
|
|
1285
|
+
const feitos = (st.checklist || []).filter(i => i.passes).length;
|
|
1286
|
+
const tot = (st.checklist || []).length;
|
|
1287
|
+
const gasto = budgetTotal > 0 ? `${st.creditsSpent} de ${budgetTotal}` : `${st.creditsSpent}`;
|
|
1288
|
+
console.log('\n ' + C.warn('🌙') + ' ' + C.bold(`Janela ${janela} concluída`) + C.dim(` · ${feitos}/${tot} itens (${pend} restantes) · ${gasto} créditos gastos · aguardando ${intervaloMin} min pra próxima janela…`) + '\n');
|
|
1289
|
+
await _sleep(intervaloMin * 60000);
|
|
1290
|
+
}
|
|
1227
1291
|
if (!st) { console.error(ui.infoLine(T.meta_none)); process.exit(2); }
|
|
1228
1292
|
|
|
1229
1293
|
const done = st.checklist.filter(i => i.passes).length;
|
|
@@ -1247,6 +1311,15 @@ async function metaCmd() {
|
|
|
1247
1311
|
console.log(ui.infoLine(st.pause_reason === 'budget' ? T.meta_paused_budget : T.meta_paused_rounds));
|
|
1248
1312
|
metaMod.notify(token, T.meta_notify_paused(st.goal, done, st.checklist.length, st.pause_reason));
|
|
1249
1313
|
}
|
|
1314
|
+
if (noturno && st._noturnoStop) {
|
|
1315
|
+
const why = st._noturnoStop === 'stall' ? 'sem progresso em 2 janelas seguidas — parei pra não queimar crédito girando em falso'
|
|
1316
|
+
: st._noturnoStop === 'budget-total' ? `orçamento total de ${budgetTotal} créditos esgotado`
|
|
1317
|
+
: st._noturnoStop === 'janelas' ? `limite de ${maxJanelasEff} janela(s) atingido`
|
|
1318
|
+
: `limite de ${maxHoras}h atingido`;
|
|
1319
|
+
console.log(' ' + C.warn('🌙') + ' ' + C.dim(`modo noturno encerrado: ${why} · ${janela} janela(s) rodada(s). Retome com "ts meta --noturno".`));
|
|
1320
|
+
} else if (noturno && st.status === 'done') {
|
|
1321
|
+
console.log(' ' + C.warn('🌙') + ' ' + C.dim(`modo noturno: missão concluída em ${janela} janela(s) sem você precisar rerodar na mão.`));
|
|
1322
|
+
}
|
|
1250
1323
|
if (blocked.length) {
|
|
1251
1324
|
console.log(' ' + C.err(T.meta_blocked));
|
|
1252
1325
|
for (const b of blocked) console.log(' ' + C.err('!') + ' ' + b.desc.slice(0, 80));
|
package/lib/agent.js
CHANGED
|
@@ -414,6 +414,16 @@ async function run(task, opts = {}) {
|
|
|
414
414
|
? `Sessão SÓ-LEITURA (--ler/--plano): a ferramenta "${name}" está BLOQUEADA (ela altera/roda algo). Responda apenas com base no que leu; se precisar mesmo agir, o usuário deve rodar sem --ler.`
|
|
415
415
|
: `READ-ONLY session (--ler/--plan): tool "${name}" is BLOCKED (it changes/runs things). Answer from what you read; to actually act, the user must run without --ler.` };
|
|
416
416
|
}
|
|
417
|
+
// RECUSA INSTANTÂNEA de AUTO-DESTRUTIVOS (vem ANTES do gate de aprovação): comandos que
|
|
418
|
+
// matam a própria missão (o processo do agente / o dev server que ela usa) ou a máquina
|
|
419
|
+
// são recusados NA HORA — sem askApprove, sem poll remoto no Telegram (~150s POR tentativa
|
|
420
|
+
// desperdiçados no incidente do jogo, em que o modelo insistia em "taskkill /f /im node").
|
|
421
|
+
// Só o comando LOCAL (executar_comando roda NESTE processo/cwd); o remoto tem outro pid/cwd
|
|
422
|
+
// e segue pelo gate de aprovação comum.
|
|
423
|
+
if (result === undefined && name === 'executar_comando') {
|
|
424
|
+
const sd = tools.selfDestructiveReason(String(input.comando || ''), { cwd });
|
|
425
|
+
if (sd) { onStep({ name, detail: argsShort(name, input), blocked: true }); result = { erro: sd }; }
|
|
426
|
+
}
|
|
417
427
|
// Gate de segurança: destrutivo → humano decide (só se ainda não foi bloqueado acima).
|
|
418
428
|
// Interativo pergunta no terminal; em --yes (cron) tenta APROVAÇÃO REMOTA no Telegram do dono.
|
|
419
429
|
if (result === undefined && (name === 'executar_comando' || name === 'executar_remoto') && tools.isDestructive(input.comando)) {
|
package/lib/meta.js
CHANGED
|
@@ -28,6 +28,9 @@ const MAX_VISUAL_FIXES = 5; // rodadas de polimento VISUAL (o olho crítico)
|
|
|
28
28
|
const MAX_VISUAL_FIXES_FLUTTER = 3; // Flutter: rebuild é MUITO mais pesado/lento → teto menor de polimento (economia)
|
|
29
29
|
const VISUAL_ESCALATE_AT = 2; // após N reprovações visuais, a MÃO do fix vira o modelo forte (grok) — o deepseek erra layout complexo
|
|
30
30
|
const MAX_BUILD_FIXES = 12; // rodadas extras de correção de build antes de desistir
|
|
31
|
+
const WEB_CANVAS_MOVE_MIN = 0.02; // check de MOVIMENTO do canvas web (jogo/cena 3D): diferença MÍNIMA
|
|
32
|
+
// entre 2 quadros pra a cena contar como VIVA. Abaixo disso = parada/congelada (T-pose, mixer parado,
|
|
33
|
+
// loop sem avançar o tempo). SÓ se aplica a canvas de JOGO/3D — dashboard/gráfico pode ficar estático.
|
|
31
34
|
|
|
32
35
|
// Detecta se o projeto é COMPILÁVEL e devolve como compilar + como saber que passou.
|
|
33
36
|
// Procura recursivamente (modelos costumam aninhar em uma subpasta tipo MultiApps/).
|
|
@@ -197,12 +200,31 @@ function _seedCreds(cwd) {
|
|
|
197
200
|
return (email && senha) ? { email, senha } : null;
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
// Compara N quadros PNG do MESMO recorte e devolve a MAIOR diferença relativa entre pares
|
|
204
|
+
// consecutivos (0 = quadros idênticos → cena PARADA; perto de 1 = mudou muito → cena ANIMANDO).
|
|
205
|
+
// Barato e sem libs: PNG é DEFLATE (qualquer mudança de pixel embaralha os bytes seguintes), então
|
|
206
|
+
// amostrar ~4000 bytes já separa bem "congelado" (≈0) de "animando" (alto). Não decodifica o PNG.
|
|
207
|
+
function _pngFramesDiff(frames) {
|
|
208
|
+
let maxDiff = 0;
|
|
209
|
+
for (let i = 1; i < frames.length; i++) {
|
|
210
|
+
const a = frames[i - 1], b = frames[i];
|
|
211
|
+
if (!a || !b || !a.length || !b.length) continue;
|
|
212
|
+
const n = Math.min(a.length, b.length), m = Math.max(a.length, b.length);
|
|
213
|
+
const step = Math.max(1, Math.floor(n / 4000));
|
|
214
|
+
let sampled = 0, changed = 0;
|
|
215
|
+
for (let k = 0; k < n; k += step) { sampled++; if (a[k] !== b[k]) changed++; }
|
|
216
|
+
const ratio = (sampled ? changed / sampled : 0) + (m ? (m - n) / m : 0);
|
|
217
|
+
if (ratio > maxDiff) maxDiff = ratio;
|
|
218
|
+
}
|
|
219
|
+
return maxDiff;
|
|
220
|
+
}
|
|
221
|
+
|
|
200
222
|
// ── GATE WEB (novo, pra portais/APIs Node): sobe o servidor DE VERDADE, checa se a porta
|
|
201
223
|
// responde (senão = erro de inicialização) e — com Chrome headless (puppeteer-core) — carrega
|
|
202
224
|
// a página, captura ERROS DE CONSOLE (ex: "Invalid Date") e tira PRINT pro olho criticar o
|
|
203
225
|
// visual. Se a home for uma TELA DE LOGIN, LOGA com o seed e verifica as telas AUTENTICADAS
|
|
204
226
|
// (senão dashboard/kanban quebrados passariam batido). Dá pra web a MESMA verificação real que apps têm.
|
|
205
|
-
async function webRunGate(b) {
|
|
227
|
+
async function webRunGate(b, opts = {}) {
|
|
206
228
|
const cwd = b.cwd, port = b.port || 3000, url = 'http://localhost:' + port + '/';
|
|
207
229
|
// instala deps se faltarem
|
|
208
230
|
try { if (!fs.existsSync(path.join(cwd, 'node_modules'))) { try { cp.execSync('npm install', { cwd, stdio: 'ignore', timeout: 240000 }); } catch (_) {} } } catch (_) {}
|
|
@@ -287,7 +309,7 @@ async function webRunGate(b) {
|
|
|
287
309
|
// (2) mede a COMPRESSÃO do PNG do canvas: tela preta/uniforme comprime absurdamente
|
|
288
310
|
// (bytes/pixel minúsculo); cena viva é rica. Usa o screenshot do Chrome (compositor
|
|
289
311
|
// real — captura WebGL certo, ao contrário de readPixels/drawImage que dão preto).
|
|
290
|
-
let canvasDead = null;
|
|
312
|
+
let canvasDead = null, canvasFrozen = null, eyeProblem = '', eyeCredits = 0, frameCount = 0;
|
|
291
313
|
try {
|
|
292
314
|
const cInfo = await page.evaluate(() => {
|
|
293
315
|
const cs = [...document.querySelectorAll('canvas')].map(c => { const r = c.getBoundingClientRect(); return { r, area: r.width * r.height }; }).sort((a, b) => b.area - a.area);
|
|
@@ -304,15 +326,64 @@ async function webRunGate(b) {
|
|
|
304
326
|
if (b) { b.click(); return true; } return false;
|
|
305
327
|
});
|
|
306
328
|
if (started) await new Promise(r => setTimeout(r, 2500)); // deixa o gameplay renderizar
|
|
307
|
-
// recalcula a bbox
|
|
308
|
-
|
|
329
|
+
// recalcula a bbox + detecta se é JOGO/CENA 3D (só aí exigimos MOVIMENTO — canvas de
|
|
330
|
+
// dashboard/gráfico é legitimamente estático). Sinais de jogo: contexto WebGL, lib de
|
|
331
|
+
// engine (Three/Babylon/PIXI/Phaser/PlayCanvas), botão de start clicado, ou o canvas
|
|
332
|
+
// ocupa a MAIOR parte da viewport (experiência imersiva).
|
|
333
|
+
const meta = await page.evaluate(() => {
|
|
334
|
+
const c = [...document.querySelectorAll('canvas')].map(el => { const r = el.getBoundingClientRect(); return { el, r, a: r.width * r.height }; }).sort((a, b) => b.a - a.a)[0];
|
|
335
|
+
if (!c) return null;
|
|
336
|
+
const r = c.r;
|
|
337
|
+
let webgl = false; try { webgl = !!(c.el.getContext('webgl2') || c.el.getContext('webgl') || c.el.getContext('experimental-webgl')); } catch (_) {}
|
|
338
|
+
const lib = !!(window.THREE || window.BABYLON || window.PIXI || window.Phaser || window.pc || window.PlayCanvas);
|
|
339
|
+
const vp = (window.innerWidth || 0) * (window.innerHeight || 0);
|
|
340
|
+
const coversMost = vp ? (r.width * r.height) / vp > 0.6 : false;
|
|
341
|
+
return { box: { x: Math.max(0, Math.round(r.x)), y: Math.max(0, Math.round(r.y)), width: Math.round(r.width), height: Math.round(r.height) }, webgl, lib, coversMost };
|
|
342
|
+
});
|
|
343
|
+
const box = meta && meta.box;
|
|
309
344
|
if (box && box.width > 200 && box.height > 150) {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
345
|
+
const gameLike = !!(started || (meta && (meta.webgl || meta.lib || meta.coversMost)));
|
|
346
|
+
// captura frames do MESMO clip: 1 sempre (check de tela preta); +2 espaçados ~500ms se
|
|
347
|
+
// for jogo (check de MOVIMENTO). Screenshot do compositor do Chrome (captura WebGL certo).
|
|
348
|
+
const frames = [];
|
|
349
|
+
const nFrames = gameLike ? 3 : 1;
|
|
350
|
+
for (let f = 0; f < nFrames; f++) {
|
|
351
|
+
const fb = await page.screenshot({ clip: box, type: 'png' }).catch(() => null);
|
|
352
|
+
if (fb && fb.length) frames.push(fb);
|
|
353
|
+
if (f < nFrames - 1) await new Promise(r => setTimeout(r, 500));
|
|
354
|
+
}
|
|
355
|
+
frameCount = frames.length;
|
|
356
|
+
// (1) TELA PRETA/UNIFORME (check histórico — INALTERADO): PNG comprime absurdamente.
|
|
357
|
+
if (frames[0] && frames[0].length) {
|
|
358
|
+
const bpp = frames[0].length / (box.width * box.height); // bytes de PNG por pixel
|
|
313
359
|
// < 0.02 B/px ≈ imagem quase uniforme (preta/cor sólida). Cena viva fica bem acima.
|
|
314
360
|
if (bpp < 0.02) canvasDead = { bpp: +bpp.toFixed(4), w: box.width, h: box.height, started };
|
|
315
361
|
}
|
|
362
|
+
// (2) MOVIMENTO (camada A MAIS, só p/ jogo/cena 3D): 2+ quadros ~idênticos = cena
|
|
363
|
+
// PARADA/CONGELADA (T-pose, mixer parado, loop de animação sem avançar o tempo).
|
|
364
|
+
if (!canvasDead && gameLike && frames.length >= 2) {
|
|
365
|
+
const mv = _pngFramesDiff(frames);
|
|
366
|
+
if (mv < WEB_CANVAS_MOVE_MIN) canvasFrozen = { mv: +mv.toFixed(4), w: box.width, h: box.height, started, webgl: !!(meta && meta.webgl) };
|
|
367
|
+
}
|
|
368
|
+
// (3) OLHO DE DOMÍNIO (jogo/cena 3D): se passou nos checks baratos, um modelo com VISÃO
|
|
369
|
+
// COMPARA os 2 quadros e cobra o CHECKLIST (personagem? animando? pés no chão? virado
|
|
370
|
+
// certo?). Gate DE VERDADE — reprova bloqueia o "concluído". Capado por opts.eyeGate.
|
|
371
|
+
if (!canvasDead && !canvasFrozen && gameLike && frames.length >= 2
|
|
372
|
+
&& opts.token && opts.eye && opts.eyeGate && !errs.length && !bad.length) {
|
|
373
|
+
try {
|
|
374
|
+
const prompt = 'Você é um DIRETOR TÉCNICO DE JOGOS 3D revisando 2 QUADROS consecutivos (capturados com ~0,5s de intervalo) do MESMO jogo/cena 3D rodando de verdade no navegador. COMPARE os dois quadros e responda ao CHECKLIST, item a item:\n'
|
|
375
|
+
+ '1) Há um PERSONAGEM/objeto principal visível na cena? (cena vazia / só chão / só céu = REPROVA)\n'
|
|
376
|
+
+ '2) O personagem ANIMA de verdade entre os 2 quadros (pose/membros mudando)? Personagem em T-POSE (braços rígidos abertos em cruz) ou totalmente imóvel enquanto deveria se mover = REPROVA.\n'
|
|
377
|
+
+ '3) Os PÉS estão no chão (apoiado no piso, não flutuando no ar nem afundado)?\n'
|
|
378
|
+
+ '4) O personagem está VIRADO para a direção certa (de frente / na direção do movimento — não de costas por engano, de lado, nem de cabeça pra baixo)?\n'
|
|
379
|
+
+ '5) A CÂMERA enquadra a cena (não está dentro do personagem nem apontada pro vazio)?\n'
|
|
380
|
+
+ 'Liste os problemas encontrados com o FIX técnico (ex.: chamar mixer.update(clock.getDelta()) no loop, carregar/tocar o clip com .play(), ajustar object.position.y pra apoiar os pés, girar rotation.y pra virar o personagem, reposicionar a câmera).\n'
|
|
381
|
+
+ 'TERMINE com UMA linha, exatamente um veredicto: "VEREDITO: OK" (cena viva e correta) ou "VEREDITO: REPROVADO" (qualquer item acima falhou).';
|
|
382
|
+
const ev = await _llmVision({ token: opts.token, model: opts.eye, text: prompt, images: [frames[0].toString('base64'), frames[frames.length - 1].toString('base64')], maxTokens: 900 });
|
|
383
|
+
eyeCredits += ev.credits || 0;
|
|
384
|
+
if (/VEREDITO:\s*REPROVAD/i.test(String(ev.text || ''))) eyeProblem = String(ev.text || '').trim();
|
|
385
|
+
} catch (_) {}
|
|
386
|
+
}
|
|
316
387
|
}
|
|
317
388
|
}
|
|
318
389
|
} catch (_) {}
|
|
@@ -331,16 +402,27 @@ async function webRunGate(b) {
|
|
|
331
402
|
const canvasProblem = canvasDead
|
|
332
403
|
? `O CANVAS gráfico (${canvasDead.w}×${canvasDead.h}) está renderizando PRATICAMENTE VAZIO (tela preta/uniforme)${canvasDead.started ? ', mesmo depois de clicar em iniciar' : ''} — o app NÃO está desenhando a cena. Causas típicas: câmera/posição em NaN (ex: delta indefinido no 1º frame do loop), objetos fora do frustum, erro silencioso no setup do WebGL, ou o loop de render não começou. Verifique a inicialização da câmera e do requestAnimationFrame.`
|
|
333
404
|
: '';
|
|
334
|
-
|
|
405
|
+
const canvasFrozenProblem = canvasFrozen
|
|
406
|
+
? `O CANVAS do jogo/cena 3D (${canvasFrozen.w}×${canvasFrozen.h}) está PARADO/CONGELADO: capturei ${frameCount} quadros ao longo de ~1s e eles saíram praticamente IDÊNTICOS (variação ${canvasFrozen.mv}, abaixo do mínimo ${WEB_CANVAS_MOVE_MIN} esperado numa cena viva)${canvasFrozen.started ? ', mesmo depois de clicar em iniciar' : ''}. A cena RENDERIZA mas NÃO ANIMA — sintoma clássico de personagem em T-pose, AnimationMixer não atualizado, ou loop de render sem avançar o tempo. Verifique: (1) o AnimationMixer tem uma action com .play() E é atualizado com mixer.update(clock.getDelta()) DENTRO do requestAnimationFrame; (2) o delta do 1º frame não é 0/NaN; (3) o loop chama requestAnimationFrame recursivamente (a animação não parou após 1 frame).`
|
|
407
|
+
: '';
|
|
408
|
+
const credits = eyeCredits;
|
|
409
|
+
// Falhas DETERMINÍSTICAS (erro real / tela preta / cena congelada) → correção normal via gate.
|
|
410
|
+
if (errs.length || bad.length || authProblem || canvasProblem || canvasFrozenProblem) {
|
|
335
411
|
kill();
|
|
336
|
-
return { ok: false, shot: shotFile, authed, crash: 'A verificação web falhou' + (authed ? ' (APÓS LOGIN, nas telas autenticadas)' : '') + ' (Chrome headless — corrija a CAUSA-RAIZ no código):\n'
|
|
412
|
+
return { ok: false, credits, shot: shotFile, authed, crash: 'A verificação web falhou' + (authed ? ' (APÓS LOGIN, nas telas autenticadas)' : '') + ' (Chrome headless — corrija a CAUSA-RAIZ no código):\n'
|
|
337
413
|
+ (errs.length ? 'Console/erros: ' + errs.slice(0, 8).join(' | ') + '\n' : '')
|
|
338
414
|
+ (bad.length ? 'Texto quebrado VISÍVEL na tela: ' + bad.join(', ') + ' (ex: data não formatada, valor undefined/NaN)\n' : '')
|
|
339
415
|
+ (canvasProblem ? canvasProblem + '\n' : '')
|
|
416
|
+
+ (canvasFrozenProblem ? canvasFrozenProblem + '\n' : '')
|
|
340
417
|
+ (authProblem || '') };
|
|
341
418
|
}
|
|
419
|
+
// Reprovação SUBJETIVA de DOMÍNIO pelo olho (T-pose sutil, pés no ar, virado errado, cena vazia).
|
|
420
|
+
if (eyeProblem) {
|
|
421
|
+
kill();
|
|
422
|
+
return { ok: false, visual: true, credits, shot: shotFile, authed, crash: 'A revisão da CENA 3D pelo olho crítico (visão, comparando 2 quadros reais do jogo rodando) REPROVOU. Corrija a CAUSA-RAIZ no código do jogo:\n' + eyeProblem };
|
|
423
|
+
}
|
|
342
424
|
kill();
|
|
343
|
-
return { ok: true, shot: shotFile, authed };
|
|
425
|
+
return { ok: true, credits, shot: shotFile, authed };
|
|
344
426
|
} catch (_) { /* puppeteer falhou → segue só com a verificação do servidor */ }
|
|
345
427
|
}
|
|
346
428
|
kill();
|
|
@@ -1026,14 +1108,21 @@ async function run(goal, opts = {}) {
|
|
|
1026
1108
|
} else if (runGate && b.kind === 'web') {
|
|
1027
1109
|
// ── GATE WEB: sobe o servidor de verdade + Chrome headless (console/erros/print).
|
|
1028
1110
|
onRound({ n: st.rounds.length + 1, item: (lang === 'en' ? 'Starting the web server and testing in the browser (web gate)…' : 'Subindo o servidor web e testando no navegador (gate web)…'), attempt: 1 });
|
|
1029
|
-
|
|
1111
|
+
// eyeGate: libera o olho de domínio (jogo/cena 3D) enquanto não estourou o teto de
|
|
1112
|
+
// polimento visual — assim a crítica cara de cena roda no máx MAX_VISUAL_FIXES vezes.
|
|
1113
|
+
let wg = { ok: true }; try { wg = await webRunGate(b, { token, eye, eyeGate: !!eye && (st.visualFixes || 0) < MAX_VISUAL_FIXES }); } catch (_) {}
|
|
1114
|
+
st.creditsSpent += wg.credits || 0;
|
|
1030
1115
|
if (wg.ok) {
|
|
1031
1116
|
st.buildVerified = true; st.runVerified = true; st.visualOk = true;
|
|
1032
1117
|
if (wg.shot) st.webShot = wg.shot;
|
|
1033
1118
|
st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st;
|
|
1034
1119
|
} else {
|
|
1035
1120
|
st.buildFixes = (st.buildFixes || 0) + 1;
|
|
1036
|
-
|
|
1121
|
+
if (wg.visual) { st.visualFixes = (st.visualFixes || 0) + 1; onAlert({ type: 'design', text: `👁 ts: o olho crítico REPROVOU a CENA 3D do jogo (${st.visualFixes}/${MAX_VISUAL_FIXES}) — personagem/animação/enquadramento. Mandando corrigir e vou reavaliar.` }); }
|
|
1122
|
+
errText = (wg.visual
|
|
1123
|
+
? 'GATE WEB (CENA 3D) — o jogo SOBE mas a CENA foi REPROVADA pelo olho crítico comparando 2 quadros reais. Corrija a CAUSA-RAIZ no código do jogo (Three.js/WebGL):\n'
|
|
1124
|
+
: 'GATE WEB — o servidor precisa SUBIR e a página abrir SEM erro de console/runtime, e — se for jogo/cena 3D — a cena precisa ANIMAR de verdade (não pode ficar em T-pose/congelada). Corrija a CAUSA-RAIZ (backend, frontend ou loop de animação):\n')
|
|
1125
|
+
+ wg.crash;
|
|
1037
1126
|
st.buildError = errText;
|
|
1038
1127
|
}
|
|
1039
1128
|
} else {
|
package/lib/tools.js
CHANGED
|
@@ -205,6 +205,119 @@ function _guardTsHome(p) {
|
|
|
205
205
|
return null;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// ── RECUSA INSTANTÂNEA de comandos AUTO-DESTRUTIVOS ──────────────────────────
|
|
209
|
+
// Diferente do gate destrutivo comum (que PEDE aprovação humana): estes comandos matam a
|
|
210
|
+
// PRÓPRIA missão — o processo do agente, o dev server de que a missão depende — ou a
|
|
211
|
+
// máquina inteira. Esperar aprovação (local, ou remota via Telegram ~150s POR tentativa)
|
|
212
|
+
// por algo que JAMAIS deve rodar é desperdício puro: recusamos NA HORA, apontando a
|
|
213
|
+
// alternativa cirúrgica. Retorna a mensagem (motivo pro modelo) quando é auto-destrutivo,
|
|
214
|
+
// ou null quando é seguro (aí segue o fluxo normal, inclusive o gate de aprovação comum).
|
|
215
|
+
function selfDestructiveReason(cmd, opts = {}) {
|
|
216
|
+
const c = String(cmd || '');
|
|
217
|
+
if (!c.trim()) return null;
|
|
218
|
+
|
|
219
|
+
// 1) Desligar/reiniciar a máquina — derruba tudo, nunca ajuda a tarefa.
|
|
220
|
+
if (/(?:^|[\s;&|(])(?:sudo\s+)?(shutdown|reboot|halt|poweroff)\b/i.test(c) || /\b(Stop|Restart)-Computer\b/i.test(c))
|
|
221
|
+
return 'AUTO-DESTRUTIVO recusado na hora: desligar/reiniciar a máquina (shutdown/reboot) aborta a missão e derruba tudo — nunca ajuda a tarefa. Não faça.';
|
|
222
|
+
|
|
223
|
+
// 2) Matar Node em MASSA (por NOME) — mata ESTE agente E o dev server da missão de uma vez.
|
|
224
|
+
if (/\btaskkill\b[^\n]*\/im\s*:?\s*"?node(\.exe)?"?/i.test(c))
|
|
225
|
+
return 'AUTO-DESTRUTIVO recusado na hora: "taskkill /IM node" mata TODOS os node.exe — inclusive ESTE agente e o dev server da missão. Para reiniciar UM servidor, ache o PID que ouve a porta ("netstat -ano | findstr :PORTA") e mate SÓ ele ("taskkill /F /PID <pid>").';
|
|
226
|
+
if (/\b(killall|pkill)\b[^\n]*\bnode\b/i.test(c))
|
|
227
|
+
return 'AUTO-DESTRUTIVO recusado na hora: matar node em massa (killall/pkill node) derruba ESTE agente e o dev server da missão. Mate só o processo da porta certa (ex: "lsof -ti:PORTA | xargs kill").';
|
|
228
|
+
if (/\bStop-Process\b[^\n]*-Name\s*"?node/i.test(c))
|
|
229
|
+
return 'AUTO-DESTRUTIVO recusado na hora: "Stop-Process -Name node" mata TODOS os node — inclusive a própria missão. Use -Id <pid> do processo certo.';
|
|
230
|
+
|
|
231
|
+
// 3) Matar o PRÓPRIO PID (o processo desta run) ou o próprio shell.
|
|
232
|
+
const selfPid = String(opts.pid || process.pid);
|
|
233
|
+
if (new RegExp('\\bkill\\b(?:\\s+-[a-z0-9]+)*\\s+' + selfPid + '\\b', 'i').test(c) ||
|
|
234
|
+
new RegExp('\\btaskkill\\b[^\\n]*\\/pid\\s+' + selfPid + '\\b', 'i').test(c))
|
|
235
|
+
return 'AUTO-DESTRUTIVO recusado na hora: ' + selfPid + ' é o PID do PRÓPRIO processo do agente — matá-lo aborta a missão. Mate só o PID do processo alvo.';
|
|
236
|
+
if (/\bkill\b(?:\s+-[a-z0-9]+)*\s+\$\$/i.test(c) || /\bkill\s+-?9?\s*-1\b/.test(c))
|
|
237
|
+
return 'AUTO-DESTRUTIVO recusado na hora: "kill $$"/"kill -1" derruba o próprio shell da missão. Mate só o PID do processo alvo.';
|
|
238
|
+
|
|
239
|
+
// 4) Apagar em massa a RAIZ do sistema, o HOME, ou a própria pasta de trabalho da missão.
|
|
240
|
+
const nukeDir = (/\brm\b/i.test(c) && /(?:\s-[a-z]*r|--recursive)/i.test(c))
|
|
241
|
+
|| /\b(rd|rmdir)\b[^\n]*\/s\b/i.test(c) || /\bdel\b[^\n]*\/s\b/i.test(c)
|
|
242
|
+
|| /\bRemove-Item\b[^\n]*-(Recurse|R|Force)\b/i.test(c);
|
|
243
|
+
if (nukeDir) {
|
|
244
|
+
// normaliza separador (\ e /) → "/" pra comparar caminhos sem depender do SO
|
|
245
|
+
const norm = (s) => String(s || '').replace(/[\\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
246
|
+
const home = norm(os.homedir());
|
|
247
|
+
const cwd = String(opts.cwd || '').replace(/[\\\/]+$/, '');
|
|
248
|
+
const cwdLow = norm(opts.cwd);
|
|
249
|
+
// tira o verbo e as flags (unix -rf / windows /s /q) — sobra a lista de ALVOS
|
|
250
|
+
const rest = c.replace(/^[\s\S]*?\b(rm|rd|rmdir|del|Remove-Item)\b/i, '');
|
|
251
|
+
const toks = rest.split(/[\s,;]+/).map(t => t.replace(/^["']|["']$/g, '')).filter(Boolean)
|
|
252
|
+
.filter(t => !/^-/.test(t) && !/^\/[a-z]$/i.test(t));
|
|
253
|
+
for (const raw of toks) {
|
|
254
|
+
const tl = raw.toLowerCase();
|
|
255
|
+
// atalhos catastróficos literais (raiz, home, cwd inteiro, curinga)
|
|
256
|
+
if (['/', '/*', '.', './', './*', '*', '~', '~/', '~\\', '$home', '${home}', '%userprofile%', '$env:userprofile', 'c:', 'c:\\', 'c:/', 'c:\\*'].includes(tl))
|
|
257
|
+
return 'AUTO-DESTRUTIVO recusado na hora: apagar "' + raw + '" em modo recursivo destruiria a raiz do sistema / seu HOME / a própria pasta da missão. Apague só a SUBPASTA específica (caminho completo até ela), nunca a raiz nem um atalho.';
|
|
258
|
+
let abs;
|
|
259
|
+
if (raw === '~' || raw.startsWith('~/') || raw.startsWith('~\\')) abs = path.join(os.homedir(), raw.slice(1));
|
|
260
|
+
else { try { abs = path.resolve(cwd || process.cwd(), raw); } catch (_) { continue; } }
|
|
261
|
+
const absLow = norm(abs);
|
|
262
|
+
if (home && absLow === home)
|
|
263
|
+
return 'AUTO-DESTRUTIVO recusado na hora: "' + raw + '" é a sua pasta HOME — apagá-la levaria a config do ts e seus arquivos junto. Apague só a subpasta específica.';
|
|
264
|
+
if (/^([a-z]:)?$/i.test(absLow))
|
|
265
|
+
return 'AUTO-DESTRUTIVO recusado na hora: "' + raw + '" aponta pra raiz do disco. Apague só a subpasta específica.';
|
|
266
|
+
// é o próprio cwd da missão OU um ancestral dele (apagar isso mata a missão + o dev server)
|
|
267
|
+
if (cwdLow && (absLow === cwdLow || (cwdLow + '/').startsWith(absLow + '/')))
|
|
268
|
+
return 'AUTO-DESTRUTIVO recusado na hora: "' + raw + '" é a própria pasta de trabalho da missão (ou um ancestral dela) — apagá-la aborta a missão e o dev server. Apague só as subpastas/arquivos específicos DENTRO dela.';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── INTROSPECÇÃO: sugerir o nome REAL mais próximo ───────────────────────────
|
|
275
|
+
// O modelo costuma CHUTAR nomes (tool "buscar_arquivo" quando é "buscar_arquivos";
|
|
276
|
+
// arquivo "input.js" quando é "Input.js"). Distância de edição (Levenshtein) simples,
|
|
277
|
+
// sem lib: acha o candidato mais parecido pra devolver na mensagem de erro — o modelo
|
|
278
|
+
// se corrige na hora, sem gastar rodadas com "não encontrado" seco.
|
|
279
|
+
function _levenshtein(a, b) {
|
|
280
|
+
a = String(a); b = String(b);
|
|
281
|
+
const m = a.length, n = b.length;
|
|
282
|
+
if (!m) return n; if (!n) return m;
|
|
283
|
+
let prev = new Array(n + 1); for (let j = 0; j <= n; j++) prev[j] = j;
|
|
284
|
+
for (let i = 1; i <= m; i++) {
|
|
285
|
+
let cur = [i];
|
|
286
|
+
for (let j = 1; j <= n; j++) {
|
|
287
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
288
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
|
|
289
|
+
}
|
|
290
|
+
prev = cur;
|
|
291
|
+
}
|
|
292
|
+
return prev[n];
|
|
293
|
+
}
|
|
294
|
+
// candidatos "perto o suficiente" do nome chutado (limiar cresce com o tamanho do nome)
|
|
295
|
+
function _maisParecidos(nome, candidatos, max = 3) {
|
|
296
|
+
const nm = String(nome || '').toLowerCase();
|
|
297
|
+
if (!nm) return [];
|
|
298
|
+
const lim = Math.max(2, Math.ceil(nm.length * 0.4));
|
|
299
|
+
return candidatos
|
|
300
|
+
.map(c => ({ c, d: _levenshtein(nm, String(c).toLowerCase()) }))
|
|
301
|
+
.filter(x => x.d <= lim)
|
|
302
|
+
.sort((a, b) => a.d - b.d)
|
|
303
|
+
.slice(0, max)
|
|
304
|
+
.map(x => x.c);
|
|
305
|
+
}
|
|
306
|
+
// nomes parecidos com o basename NO MESMO diretório (pra erro "arquivo/pasta não encontrado")
|
|
307
|
+
function _vizinhos(absPath) {
|
|
308
|
+
try {
|
|
309
|
+
const dir = path.dirname(absPath);
|
|
310
|
+
const alvo = path.basename(absPath);
|
|
311
|
+
const nomes = fs.readdirSync(dir);
|
|
312
|
+
let cand = _maisParecidos(alvo, nomes, 5);
|
|
313
|
+
if (!cand.length) { // fallback por substring / mesmo radical (antes da extensão)
|
|
314
|
+
const al = alvo.toLowerCase();
|
|
315
|
+
cand = nomes.filter(n => { const nl = n.toLowerCase(); return nl.includes(al) || al.includes(nl) || nl.split('.')[0] === al.split('.')[0]; }).slice(0, 5);
|
|
316
|
+
}
|
|
317
|
+
return cand;
|
|
318
|
+
} catch (_) { return []; }
|
|
319
|
+
}
|
|
320
|
+
|
|
208
321
|
async function execute(name, input, opts = {}) {
|
|
209
322
|
// BASE de trabalho: cwd da sessão do agente (opts.baseDir) → pasta confinada da
|
|
210
323
|
// missão (opts.confineDir) → process.cwd(). É a raiz de todo caminho relativo.
|
|
@@ -217,6 +330,9 @@ async function execute(name, input, opts = {}) {
|
|
|
217
330
|
switch (name) {
|
|
218
331
|
case 'executar_comando': {
|
|
219
332
|
const comando = String(input.comando || '');
|
|
333
|
+
// RECUSA INSTANTÂNEA (defesa em profundidade — o gate do agent.js já pega antes):
|
|
334
|
+
// comando auto-destrutivo NUNCA roda e NUNCA espera aprovação.
|
|
335
|
+
{ const sd = selfDestructiveReason(comando, { cwd: baseDir }); if (sd) return { erro: sd }; }
|
|
220
336
|
const timeout = Math.min(300, Math.max(5, Number(input.timeout_s) || 60)) * 1000;
|
|
221
337
|
const cmd = process.platform === 'win32' ? `chcp 65001>nul & ${comando}` : comando;
|
|
222
338
|
return await new Promise((res) => {
|
|
@@ -251,6 +367,12 @@ async function execute(name, input, opts = {}) {
|
|
|
251
367
|
case 'ler_arquivo': {
|
|
252
368
|
const p = _abs(input.caminho, baseDir);
|
|
253
369
|
if (BIN_RE.test(p)) return { erro: 'Arquivo binário — ler_arquivo só lê texto. Use executar_comando com uma ferramenta adequada.' };
|
|
370
|
+
// Não achou? Sugere nomes PARECIDOS na mesma pasta (o modelo chuta o nome do recurso).
|
|
371
|
+
if (!fs.existsSync(p)) {
|
|
372
|
+
const v = _vizinhos(p);
|
|
373
|
+
return { erro: 'Arquivo não encontrado: ' + p + (v.length ? '. Nomes parecidos na mesma pasta: ' + v.join(', ') + ' — confira o nome EXATO (maiúsculas/plural contam).' : '. Confira o caminho (use listar_diretorio/buscar_arquivos).') };
|
|
374
|
+
}
|
|
375
|
+
try { if (fs.statSync(p).isDirectory()) return { erro: p + ' é uma PASTA, não um arquivo. Use listar_diretorio pra ver o conteúdo.' }; } catch (_) {}
|
|
254
376
|
const txt = fs.readFileSync(p, 'utf8');
|
|
255
377
|
const linhas = txt.split('\n'); const total = linhas.length; const MAXL = 400;
|
|
256
378
|
const ini = parseInt(input.inicio) || 0;
|
|
@@ -301,7 +423,10 @@ async function execute(name, input, opts = {}) {
|
|
|
301
423
|
p = path.join(base, i >= 0 ? parts.slice(i).join(path.sep) : parts[parts.length - 1]);
|
|
302
424
|
}
|
|
303
425
|
}
|
|
304
|
-
if (!fs.existsSync(p))
|
|
426
|
+
if (!fs.existsSync(p)) {
|
|
427
|
+
const v = _vizinhos(p);
|
|
428
|
+
return { erro: 'Arquivo não existe: ' + p + (v.length ? '. Nomes parecidos na mesma pasta: ' + v.join(', ') + ' — confira se não errou o nome.' : '') + ' Pra criar arquivo NOVO use escrever_arquivo.' };
|
|
429
|
+
}
|
|
305
430
|
const orig = fs.readFileSync(p, 'utf8');
|
|
306
431
|
let buscar = String(input.buscar ?? ''), substituir = String(input.substituir ?? '');
|
|
307
432
|
if (!buscar) return { erro: 'buscar vazio.' };
|
|
@@ -347,7 +472,7 @@ async function execute(name, input, opts = {}) {
|
|
|
347
472
|
// Muda o cwd da SESSÃO. O loop do agente lê _setCwd e passa a resolver os
|
|
348
473
|
// próximos caminhos relativos (e comandos) a partir daqui.
|
|
349
474
|
const alvo = _abs(input.caminho || input.diretorio || '.', baseDir);
|
|
350
|
-
let st; try { st = fs.statSync(alvo); } catch (_) { return { erro: 'Pasta não existe: ' + alvo + '. Confira o caminho (use listar_diretorio/buscar_arquivos).' }; }
|
|
475
|
+
let st; try { st = fs.statSync(alvo); } catch (_) { const v = _vizinhos(alvo); return { erro: 'Pasta não existe: ' + alvo + (v.length ? '. Nomes parecidos na pasta acima: ' + v.join(', ') : '') + '. Confira o caminho (use listar_diretorio/buscar_arquivos).' }; }
|
|
351
476
|
if (!st.isDirectory()) return { erro: 'Não é uma pasta: ' + alvo };
|
|
352
477
|
return { ok: true, cwd: alvo, _setCwd: alvo };
|
|
353
478
|
}
|
|
@@ -440,13 +565,16 @@ async function execute(name, input, opts = {}) {
|
|
|
440
565
|
hubs: m.hubs, mapa: m.edges.map(([a, b]) => a + ' -> ' + b) };
|
|
441
566
|
}
|
|
442
567
|
default: {
|
|
443
|
-
// sugere o nome REAL mais parecido (o modelo às vezes inventa uma variante)
|
|
568
|
+
// sugere o nome REAL mais parecido (o modelo às vezes inventa uma variante):
|
|
569
|
+
// 1) substring óbvia, 2) menor distância de edição, 3) mesmo radical antes do "_".
|
|
444
570
|
const nomes = DEFS.map(d => d.function.name);
|
|
445
|
-
const alvo = nomes.find(n => n.includes(name) || name.includes(n))
|
|
571
|
+
const alvo = nomes.find(n => n.includes(name) || name.includes(n))
|
|
572
|
+
|| _maisParecidos(name, nomes, 1)[0]
|
|
573
|
+
|| nomes.find(n => n.split('_')[0] === String(name).split('_')[0]);
|
|
446
574
|
return { erro: 'Ferramenta desconhecida: ' + name + (alvo ? ('. Você quis dizer "' + alvo + '"? Use esse nome EXATO.') : ('. Ferramentas válidas: ' + nomes.join(', '))) };
|
|
447
575
|
}
|
|
448
576
|
}
|
|
449
577
|
} catch (e) { return { erro: String((e && e.message) || e).slice(0, 400) }; }
|
|
450
578
|
}
|
|
451
579
|
|
|
452
|
-
module.exports = { DEFS, execute, isDestructive, buildProjectMap };
|
|
580
|
+
module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, buildProjectMap };
|