terminal-smart-cli 0.37.0 → 0.39.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/lib/agent.js CHANGED
@@ -54,6 +54,7 @@ REGRAS:
54
54
  - TRABALHE SOMENTE dentro do DIRETÓRIO DE TRABALHO (informado no fim). Crie todos os arquivos com caminho ABSOLUTO começando por ele. NUNCA invente outra pasta (ex: NÃO use AndroidStudioProjects, Documents, Desktop, nem C:\\ raiz). "nesta pasta"/"aqui" = o DIRETÓRIO DE TRABALHO.
55
55
  - CÓDIGO MÍNIMO (economia): antes de codar pergunte — precisa existir? já existe aqui (reuse)? a plataforma já faz nativo (use o nativo)? Só então escreva o MÍNIMO que resolve; sem lib extra sem necessidade real, sem abstração gratuita.
56
56
  - TAREFA EM LOTE/REPETITIVA (processar vários arquivos, renomear, migrar, contar, buscar-e-substituir): ESCREVA UM script (.py/.js/.sh) que faça o trabalho INTEIRO e rode UMA vez, lendo o resultado do stdout — NÃO itere ferramenta-por-item (é mais barato, mais rápido e mais confiável).
57
+ - PERSONAGEM 3D num jogo web (humano/animal/criatura): NUNCA modele com primitivas (BoxGeometry/SphereGeometry = fica quadrado/feio). CARREGUE um GLB RIGADO com animação via GLTFLoader + AnimationMixer. Personagens CC0 prontos (com Idle/Walk/Run, CORS liberado): https://terminalsmart.com.br/assets/chars/RobotExpressive.glb e /Soldier.glb. Instale a skill "personagem-3d-web" (ts skills add personagem-3d-web) pro passo a passo. Cenário/obstáculos podem ser primitivas; PERSONAGEM não.
57
58
  - INVESTIGAR ("onde está X?", "como o projeto faz Y?", ler MUITOS arquivos pra entender): use a ferramenta explorar (sub-agente só-leitura) — ela lê tudo em contexto separado e te devolve só o RESUMO, economizando o seu contexto. Não abra 10 arquivos você mesmo.
58
59
  - Prefira comandos de LEITURA para diagnosticar antes de alterar qualquer coisa.
59
60
  - Comandos destrutivos passam por aprovação do usuário; se negado, explique e proponha alternativa segura.
@@ -69,6 +70,7 @@ RULES:
69
70
  - WORK ONLY inside the WORKING DIRECTORY (shown at the end). Create every file with an ABSOLUTE path starting with it. NEVER invent another folder (e.g. do NOT use AndroidStudioProjects, Documents, Desktop, or C:\\ root). "this folder"/"here" = the WORKING DIRECTORY.
70
71
  - MINIMAL CODE (economy): before coding ask — does it need to exist? does it already exist here (reuse)? does the platform do it natively (use that)? Only then write the MINIMUM that solves it; no extra libs without real need, no gratuitous abstraction.
71
72
  - BATCH/REPEATED TASK (process many files, rename, migrate, count, find-and-replace): WRITE a script (.py/.js/.sh) that does the WHOLE job and run it ONCE, reading the result from stdout — do NOT iterate tool-by-item (cheaper, faster, more reliable).
73
+ - 3D CHARACTER in a web game (human/animal/creature): NEVER model it with primitives (BoxGeometry/SphereGeometry = blocky/ugly). LOAD a RIGGED GLB with animation via GLTFLoader + AnimationMixer. Ready CC0 characters (Idle/Walk/Run, CORS enabled): https://terminalsmart.com.br/assets/chars/RobotExpressive.glb and /Soldier.glb. Install the "personagem-3d-web" skill for the full recipe. Scenery/obstacles can be primitives; the CHARACTER must not.
72
74
  - Prefer READ commands to diagnose before changing anything.
73
75
  - Destructive commands go through user approval; if denied, explain and offer a safe alternative.
74
76
  - NEVER expose or ask for secrets (.env, keys, passwords, tokens); never send data off this machine.
package/lib/browser.js CHANGED
@@ -62,8 +62,13 @@ class BrowserSession {
62
62
  const u = String(url || '').trim();
63
63
  if (!/^https?:\/\//i.test(u)) throw new Error('só URLs http(s) são permitidas (sem file://, data: etc).');
64
64
  if (/^[a-z][a-z0-9+.-]*:\/\/[^\/\s@]*@/i.test(u)) throw new Error('URL com credencial embutida (user:senha@) é recusada.');
65
- try { await this.page.goto(u, { waitUntil: 'networkidle2', timeout: 45000 }); }
66
- catch (e) { throw new Error('falha ao carregar a página: ' + String(e.message || e).slice(0, 120)); }
65
+ // domcontentloaded, NÃO networkidle2: páginas com rAF/polling (jogos, SPAs, WebGL) nunca
66
+ // deixam a rede "acalmar" networkidle2 travava até o timeout de 45s. Carrega o DOM e
67
+ // um tempo curto pra montar; se a rede acalmar antes, melhor ainda.
68
+ try {
69
+ await this.page.goto(u, { waitUntil: 'domcontentloaded', timeout: 25000 });
70
+ await this.page.waitForNetworkIdle({ idleTime: 600, timeout: 4000 }).catch(() => {});
71
+ } catch (e) { throw new Error('falha ao carregar a página: ' + String(e.message || e).slice(0, 120)); }
67
72
  return this.snapshot();
68
73
  }
69
74
 
package/lib/meta.js CHANGED
@@ -243,7 +243,9 @@ async function webRunGate(b) {
243
243
  // 401/403 = desafio de auth NORMAL (a SPA chama rota protegida antes de logar) — não é bug.
244
244
  // 500+ = erro de servidor REAL; 404 = rota/recurso faltando. Só esses reprovam.
245
245
  page.on('response', r => { const s = r.status(); const u = r.url(); if ((s >= 500 || s === 404) && !_benign(u)) errs.push(`HTTP ${s} em ${u.replace(/^https?:\/\/[^/]+/, '').slice(0, 100)}`); });
246
- await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 }).catch(() => {});
246
+ // domcontentloaded (não networkidle2): jogos/SPAs com rAF ou polling nunca "acalmam" a
247
+ // rede → networkidle2 travava até o timeout. Carrega o DOM e dá um tempo fixo pra montar.
248
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => {});
247
249
  await new Promise(r => setTimeout(r, 1800));
248
250
 
249
251
  // GATE AUTENTICADO: se a home tem campo de senha (=tela de login), LOGA com o seed e deixa
@@ -277,6 +279,44 @@ async function webRunGate(b) {
277
279
  }
278
280
  } catch (_) {}
279
281
 
282
+ // ── GATE VISUAL DE CANVAS (jogos/apps gráficos) ──────────────────────────
283
+ // Ponto cego histórico: um jogo Three.js/canvas podia renderizar PRETO a tela
284
+ // inteira (ex: câmera em NaN) SEM lançar erro de JS e SEM texto quebrado → o gate
285
+ // aprovava um app visualmente morto. Fix: se há um canvas grande, (1) clica num
286
+ // botão de START (jogar/play/iniciar) pra sair do menu e exercitar o render, e
287
+ // (2) mede a COMPRESSÃO do PNG do canvas: tela preta/uniforme comprime absurdamente
288
+ // (bytes/pixel minúsculo); cena viva é rica. Usa o screenshot do Chrome (compositor
289
+ // real — captura WebGL certo, ao contrário de readPixels/drawImage que dão preto).
290
+ let canvasDead = null;
291
+ try {
292
+ const cInfo = await page.evaluate(() => {
293
+ 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);
294
+ const big = cs[0];
295
+ if (!big || big.r.width < 300 || big.r.height < 220) return null; // sem canvas grande = não é app gráfico
296
+ return { x: Math.round(big.r.x), y: Math.round(big.r.y), w: Math.round(big.r.width), h: Math.round(big.r.height) };
297
+ });
298
+ if (cInfo) {
299
+ // sai do menu: clica um botão de start VISÍVEL (não é login — esse já foi tratado)
300
+ const started = await page.evaluate(() => {
301
+ const rx = /\b(jogar|começar|comecar|iniciar|start|play|new game|novo jogo|entrar)\b/i;
302
+ const btns = [...document.querySelectorAll('button, [role=button], a.btn, .btn, #start, [id*=start i], [class*=start i]')];
303
+ const b = btns.find(el => el.offsetParent !== null && rx.test((el.innerText || el.value || el.getAttribute('aria-label') || '').trim()));
304
+ if (b) { b.click(); return true; } return false;
305
+ });
306
+ if (started) await new Promise(r => setTimeout(r, 2500)); // deixa o gameplay renderizar
307
+ // recalcula a bbox (o canvas pode ter mudado de tamanho ao iniciar)
308
+ const box = await page.evaluate(() => { const c = [...document.querySelectorAll('canvas')].map(c => { const r = c.getBoundingClientRect(); return { r, a: r.width * r.height }; }).sort((a, b) => b.a - a.a)[0]; if (!c) return null; const r = c.r; return { 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) }; });
309
+ if (box && box.width > 200 && box.height > 150) {
310
+ const buf = await page.screenshot({ clip: box, type: 'png' }).catch(() => null);
311
+ if (buf && buf.length) {
312
+ const bpp = buf.length / (box.width * box.height); // bytes de PNG por pixel
313
+ // < 0.02 B/px ≈ imagem quase uniforme (preta/cor sólida). Cena viva fica bem acima.
314
+ if (bpp < 0.02) canvasDead = { bpp: +bpp.toFixed(4), w: box.width, h: box.height, started };
315
+ }
316
+ }
317
+ }
318
+ } catch (_) {}
319
+
280
320
  shotFile = path.join(_os.tmpdir(), 'ts-web-' + port + '.png');
281
321
  try { await page.screenshot({ path: shotFile, fullPage: false }); } catch (_) { shotFile = null; }
282
322
  let bodyText = ''; try { bodyText = await page.evaluate(() => (document.body ? document.body.innerText : '')); } catch (_) {}
@@ -288,11 +328,15 @@ async function webRunGate(b) {
288
328
  const authProblem = (hasLogin && !authed)
289
329
  ? 'Há uma TELA DE LOGIN mas o gate NÃO conseguiu ENTRAR' + (creds ? ' com o usuário de seed "' + creds.email + '"' : ' (nenhum usuário de SEED encontrado no código)') + '. Crie no seed um usuário demo FUNCIONAL (email+senha em texto no script) e garanta que o login aceita ele — senão dashboard/kanban e o resto ficam sem verificação.'
290
330
  : '';
291
- if (errs.length || bad.length || authProblem) {
331
+ const canvasProblem = canvasDead
332
+ ? `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
+ : '';
334
+ if (errs.length || bad.length || authProblem || canvasProblem) {
292
335
  kill();
293
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'
294
337
  + (errs.length ? 'Console/erros: ' + errs.slice(0, 8).join(' | ') + '\n' : '')
295
338
  + (bad.length ? 'Texto quebrado VISÍVEL na tela: ' + bad.join(', ') + ' (ex: data não formatada, valor undefined/NaN)\n' : '')
339
+ + (canvasProblem ? canvasProblem + '\n' : '')
296
340
  + (authProblem || '') };
297
341
  }
298
342
  kill();
package/lib/tools.js CHANGED
@@ -209,6 +209,10 @@ async function execute(name, input, opts = {}) {
209
209
  // BASE de trabalho: cwd da sessão do agente (opts.baseDir) → pasta confinada da
210
210
  // missão (opts.confineDir) → process.cwd(). É a raiz de todo caminho relativo.
211
211
  const baseDir = opts.baseDir || opts.confineDir || process.cwd();
212
+ // Alias de nomes que os modelos costumam alucinar no singular/variante → o nome REAL da tool
213
+ // (senão "Ferramenta desconhecida" desperdiça uma rodada). buscar_arquivo→buscar_arquivos etc.
214
+ const _ALIAS = { buscar_arquivo: 'buscar_arquivos', listar_arquivos: 'listar_diretorio', listar_dir: 'listar_diretorio', ler: 'ler_arquivo', escrever: 'escrever_arquivo', editar: 'editar_arquivo', executar: 'executar_comando', comando: 'executar_comando', shell: 'executar_comando', bash: 'executar_comando', cd: 'mudar_diretorio' };
215
+ if (_ALIAS[name]) name = _ALIAS[name];
212
216
  try {
213
217
  switch (name) {
214
218
  case 'executar_comando': {
@@ -435,7 +439,12 @@ async function execute(name, input, opts = {}) {
435
439
  return { diretorio: base, arquivos: m.arquivos, dependencias: m.dependencias,
436
440
  hubs: m.hubs, mapa: m.edges.map(([a, b]) => a + ' -> ' + b) };
437
441
  }
438
- default: return { erro: 'Ferramenta desconhecida: ' + name };
442
+ default: {
443
+ // sugere o nome REAL mais parecido (o modelo às vezes inventa uma variante)
444
+ const nomes = DEFS.map(d => d.function.name);
445
+ const alvo = nomes.find(n => n.includes(name) || name.includes(n)) || nomes.find(n => n.split('_')[0] === String(name).split('_')[0]);
446
+ return { erro: 'Ferramenta desconhecida: ' + name + (alvo ? ('. Você quis dizer "' + alvo + '"? Use esse nome EXATO.') : ('. Ferramentas válidas: ' + nomes.join(', '))) };
447
+ }
439
448
  }
440
449
  } catch (e) { return { erro: String((e && e.message) || e).slice(0, 400) }; }
441
450
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
5
5
  "bin": {
6
6
  "ts": "bin/ts.js"