terminal-smart-cli 0.92.0 → 0.93.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 CHANGED
@@ -913,8 +913,13 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
913
913
  // saída → o log ficava VAZIO enquanto o agente rodava. Nesses casos usa spinner NOOP: os passos
914
914
  // saem por console.log (visíveis no arquivo/pipe) e o progresso aparece de verdade.
915
915
  const _noTTY = !process.stdout.isTTY;
916
+ // B4: headless (não-TTY, redirect >arquivo/pipe) — o console.log é bufferizado em BLOCO e o
917
+ // log fica VAZIO durante a run (impossível acompanhar ao vivo; foi o que escondeu o hang de
918
+ // 40min no PW). _hlog escreve SÍNCRONO no fd 1 (fs.writeSync), descarregando cada linha na
919
+ // hora. No TTY segue o console.log normal (o spinner cuida do redesenho).
920
+ const _hlog = (s) => { if (_noTTY && !streamJson) { try { require('fs').writeSync(1, s + '\n'); return; } catch (_) {} } console.log(s); };
916
921
  const sp = (streamJson || _noTTY) ? { text() {}, start() {}, stop() {} } : ui.spinner(T.agent_thinking).start();
917
- if (_noTTY && !streamJson) console.log(' ' + C.dim(T.agent_thinking));
922
+ if (_noTTY && !streamJson) _hlog(' ' + C.dim(T.agent_thinking));
918
923
  const t0 = Date.now();
919
924
  // STATUS LINE ao vivo (estilo Claude/Verboo): tempo · modelo · tokens · passo — atualiza a cada 1s.
920
925
  const _live = { steps: 0, inTok: 0, outTok: 0, model: model || 'auto' };
@@ -950,13 +955,13 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
950
955
  const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('›');
951
956
  // nome AMIGÁVEL (Rodando no servidor…) em vez do técnico (executar_remoto); detalhe sem cortar palavra
952
957
  const _label = ui.friendlyTool(name, cfg.lang === 'en');
953
- console.log(' ' + tag + ' ' + C.bold(_label) + (detail ? C.dim(' · ' + ui.wordTrunc(detail, 60)) : ''));
958
+ _hlog(' ' + tag + ' ' + C.bold(_label) + (detail ? C.dim(' · ' + ui.wordTrunc(detail, 60)) : ''));
954
959
  sp.start();
955
960
  },
956
961
  onStepDone: (e) => {
957
962
  if (streamJson) return; // o evento 'result' já cobre; aqui é só a UI bonita
958
963
  sp.stop();
959
- console.log(stepDoneLine(e, ' '));
964
+ _hlog(stepDoneLine(e, ' '));
960
965
  sp.start();
961
966
  },
962
967
  askApprove: async (payload) => {
@@ -970,7 +975,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
970
975
  onRemote: ({ ttl }) => {
971
976
  if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_remote', ttl: ttl || 120 })); return; }
972
977
  sp.stop();
973
- console.log(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120)));
978
+ _hlog(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120)));
974
979
  sp.start();
975
980
  },
976
981
  });
@@ -1247,7 +1252,10 @@ async function vpsCmd(words) {
1247
1252
  const ssh = require('../lib/ssh');
1248
1253
  const sub = (words[0] || '').toLowerCase();
1249
1254
  const rest = words.slice(1);
1250
- const flag = (n) => { const i = rest.indexOf(n); return i >= 0 ? rest[i + 1] : null; };
1255
+ // a flag do process.argv CRU: o parser de topo tira os nomes de flag (--host…) dos
1256
+ // positionais (POS), então `words` só traz os VALORES soltos e o pareamento se perdia
1257
+ // (ts vps set --host X --user Y ignorava tudo e salvava o perfil velho). O argv preserva par.
1258
+ const flag = (n) => { const i = process.argv.indexOf(n); return i >= 0 ? process.argv[i + 1] : null; };
1251
1259
 
1252
1260
  // ts vps set --host IP --user ubuntu --chave caminho.pem (ou --login arquivo.txt / --senha ...)
1253
1261
  if (sub === 'set' || sub === 'config' || sub === 'add') {
package/lib/agent.js CHANGED
@@ -73,6 +73,8 @@ REGRAS:
73
73
  - BINÁRIO NATIVO: ao subir/instalar um executável numa máquina, valide com file (arquitetura: 32 vs 64-bit) E ldd (as bibliotecas resolvem?) ANTES de declarar "pronto/instalado" — "está no lugar" NÃO é "roda". Se ldd mostrar "not found", instale a lib faltante e revalide.
74
74
  - WINDOWS / SHELL BASH: o shell aqui é o bash do Git, NÃO o cmd. (a) "node -e"/"python -c" com várias linhas ou com import() FALHA — escreva um arquivo .mjs/.py e rode "node arquivo.mjs"/"python arquivo.py". (b) pra ESPERAR use "sleep N" (NUNCA "timeout /t", que é do cmd e quebra no bash). (c) comandos que só existem no cmd (dir, type, if exist) rode via "cmd /c \"...\"". (d) pra matar um servidor/processo de teste, ache o PID pela PORTA ("netstat -ano | findstr :PORTA" → "taskkill /PID <pid> /F") — NUNCA por imagem ("taskkill /IM node.exe" mata processos não relacionados, inclusive os do usuário).
75
75
  - PROVE O CAMINHO REAL antes de declarar "pronto/corrigido": "testes de API passando" NÃO é "o app funciona". Se há UI ou rotas condicionais (por papel/role, por filtro), rode o FLUXO EXATO do usuário afetado — a tela/rota que quebrava — e veja o resultado; numa entrega WEB, verifique com olhos (navegador). Em fix de código com ramos (SQL com subqueries/parâmetros, condicional por role): conte placeholder-a-placeholder e rode de novo o caminho que falhava. Só diga "corrigido" COM a prova verde do caminho real — e NÃO peça pro usuário rodar a prova que você mesmo consegue rodar. Se subir um server de teste, ENCERRE-o (por PID) ao terminar — não deixe processo órfão na porta.
76
+ - DEPLOY EM CONTAINER: edite sempre a FONTE (o diretório do projeto no host, ex /opt/projects/<app>, ou o código local), NUNCA dentro do container (/app/... é efêmero) — o "docker build" monta a imagem a partir da FONTE, então um fix feito dentro do container SOME no rebuild e você fica achando que corrigiu. Do mesmo jeito: NUNCA inclua arquivos de segredo (.env) no pacote/tar de deploy — o .env de exemplo do repo sobrescreve o .env REAL do servidor e a app volta pro modo stub. Ao recriar um container, repasse rede, portas, volumes E o --env-file que ele já tinha.
77
+ - SEU PRÓPRIO TESTE PODE ESTAR ERRADO: antes de confiar num veredito "FALHOU", confira a ASSERÇÃO (HTTP 200/201 é SUCESSO, não falha; 401/403 numa rota protegida sem token é o comportamento CORRETO). E não re-leia/re-escreva o MESMO arquivo várias vezes: se você já leu, use o que leu.
76
78
  - ESTILO DA RESPOSTA (importante): escreva em linguagem NATURAL e direta, como explicando pra uma pessoa — evite jargão e detalhe técnico interno que não interessa a quem só quer o resultado. NÃO use emojis decorativos (nada de 🎮📡🔬🌿✅❌ etc.); pra marcar certo/errado use SÓ os símbolos ✓ (deu certo) ou ✗ (falhou). Frases curtas; comando/código/caminho sempre em bloco de código. Não repita o que já apareceu nos passos acima.
77
79
  - Termine SEMPRE com um resumo curto: o que foi feito, resultado e caminhos de arquivos criados/alterados.
78
80
  - Responda no idioma do usuário (padrão: português do Brasil).`
@@ -101,6 +103,8 @@ RULES:
101
103
  - NATIVE BINARY: when uploading/installing an executable on a machine, validate with file (architecture: 32 vs 64-bit) AND ldd (do the libraries resolve?) BEFORE declaring "done/installed" — "it's in place" is NOT "it runs". If ldd shows "not found", install the missing lib and re-validate.
102
104
  - WINDOWS / BASH SHELL: the shell here is Git bash, NOT cmd. (a) "node -e"/"python -c" with multiple lines or with import() FAILS — write a .mjs/.py file and run "node file.mjs"/"python file.py". (b) to WAIT use "sleep N" (NEVER "timeout /t", which is cmd and breaks in bash). (c) cmd-only commands (dir, type, if exist) run via "cmd /c \"...\"". (d) to kill a test server/process, find the PID by PORT ("netstat -ano | findstr :PORT" → "taskkill /PID <pid> /F") — NEVER by image ("taskkill /IM node.exe" kills unrelated processes, including the user's).
103
105
  - PROVE THE REAL PATH before declaring "done/fixed": "API tests passing" is NOT "the app works". If there's UI or conditional routes (by role, by filter), run the EXACT flow of the affected user — the screen/route that was breaking — and see the result; on a WEB deliverable, verify with eyes (browser). In a code fix with branches (SQL with subqueries/params, role conditionals): count placeholder-by-placeholder and re-run the path that was failing. Only say "fixed" WITH green proof of the real path — and do NOT ask the user to run a proof you can run yourself. If you start a test server, SHUT IT DOWN (by PID) when done — don't leave an orphan process on the port.
106
+ - CONTAINER DEPLOY: always edit the SOURCE (the project dir on the host, e.g. /opt/projects/<app>, or the local code), NEVER inside the container (/app/... is ephemeral) — "docker build" builds the image FROM THE SOURCE, so a fix made inside the container VANISHES on rebuild while you think it's fixed. Likewise: NEVER include secret files (.env) in the deploy tar/package — the repo's sample .env overwrites the REAL server .env and the app falls back to stub mode. When recreating a container, re-pass its network, ports, volumes AND the --env-file it had.
107
+ - YOUR OWN TEST MAY BE WRONG: before trusting a "FAILED" verdict, check the ASSERTION (HTTP 200/201 is SUCCESS, not failure; 401/403 on a protected route without a token is the CORRECT behavior). And don't re-read/re-write the SAME file repeatedly: if you already read it, use what you read.
104
108
  - RESPONSE STYLE (important): write in NATURAL, plain language, like explaining to a person — avoid jargon and internal technical detail that doesn't matter to someone who just wants the result. NO decorative emojis (no 🎮📡🔬✅❌ etc.); to mark pass/fail use ONLY the symbols ✓ (worked) or ✗ (failed). Short sentences; commands/code/paths always in a code block. Don't repeat what already showed up in the steps above.
105
109
  - ALWAYS end with a short summary: what was done, the result, and paths of created/changed files.
106
110
  - Answer in the user's language.`);
package/lib/core.js CHANGED
@@ -195,8 +195,38 @@ function classifyToolResult(raw) {
195
195
  return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: String(ev || '').replace(/\s+/g, ' ').slice(0, 200) };
196
196
  }
197
197
 
198
+ // ── SEGREDOS: nunca imprimir chave/token na saída (B18) ───────────────────────
199
+ // O agente vaza segredo enquanto DIAGNOSTICA (cat .env, docker inspect, echo $KEY,
200
+ // grep numa store de chaves) mesmo instruído a não fazer isso — regra sozinha não
201
+ // segura. Aqui a saída de ferramenta passa por uma máscara determinística ANTES de
202
+ // virar contexto/log. Mantém o prefixo (o agente ainda reconhece "é uma chave sk-…")
203
+ // mas o valor não circula. Não altera o que é ENVIADO ao comando, só o que é EXIBIDO.
204
+ const _SECRET_RES = [
205
+ // chaves com prefixo conhecido: sk-…, sk-cdc-…, ghp_…, xoxb-…, AKIA…, AIza…
206
+ [/\b((?:sk|pk|rk)-(?:[a-z0-9]+-)?)[A-Za-z0-9_-]{16,}/gi, (m, p) => p + '••••REDACTED'],
207
+ [/\b(gh[pousr]_)[A-Za-z0-9]{16,}/g, (m, p) => p + '••••REDACTED'],
208
+ [/\b(xox[baprs]-)[A-Za-z0-9-]{10,}/gi, (m, p) => p + '••••REDACTED'],
209
+ [/\b(AKIA)[0-9A-Z]{12,}/g, (m, p) => p + '••••REDACTED'],
210
+ [/\b(AIza)[A-Za-z0-9_-]{20,}/g, (m, p) => p + '••••REDACTED'],
211
+ // JWT (header.payload.signature)
212
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, () => 'eyJ••••REDACTED_JWT'],
213
+ // atribuição de env/JSON com nome sensível: API_KEY=…, "password": "…", Bearer …
214
+ [/((?:api[_-]?key|apikey|secret|token|password|passwd|senha|authorization|auth[_-]?token|private[_-]?key)["']?\s*[:=]\s*["']?)([^\s"',;&|)]{8,})/gi,
215
+ (m, p) => p + '••••REDACTED'],
216
+ [/\b(Bearer\s+)[A-Za-z0-9._~+/-]{16,}=*/g, (m, p) => p + '••••REDACTED'],
217
+ ];
218
+ function redactSecrets(input) {
219
+ if (input == null) return input;
220
+ if (typeof input !== 'string') {
221
+ try { return JSON.parse(redactSecrets(JSON.stringify(input))); } catch (_) { return input; }
222
+ }
223
+ let s = input;
224
+ for (const [re, fn] of _SECRET_RES) s = s.replace(re, fn);
225
+ return s;
226
+ }
227
+
198
228
  module.exports = {
199
- DESTRUCTIVE, isDestructive, touchesTsConfig,
229
+ DESTRUCTIVE, isDestructive, touchesTsConfig, redactSecrets,
200
230
  GLOBAL_STATE, globalStateWarning, describeDestructive,
201
231
  MODEL_WINDOWS, CTX_WINDOW_DEFAULT, DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok,
202
232
  AgentEvents, EVENT_TYPES,
package/lib/tools.js CHANGED
@@ -224,6 +224,25 @@ function _guardTsHome(p) {
224
224
  // por algo que JAMAIS deve rodar é desperdício puro: recusamos NA HORA, apontando a
225
225
  // alternativa cirúrgica. Retorna a mensagem (motivo pro modelo) quando é auto-destrutivo,
226
226
  // ou null quando é seguro (aí segue o fluxo normal, inclusive o gate de aprovação comum).
227
+ // Gotchas de shell no Windows que falham de um jeito CONFUSO (o agente perdia vários
228
+ // passos tentando de novo). Devolve a mensagem com o CONSERTO pronto, ou null.
229
+ function _shellGotcha(cmd) {
230
+ const s = String(cmd || '');
231
+ const multi = /[\r\n]/.test(s);
232
+ // python -c / node -e multilinha ou com import() dinâmico: quebra no eval / morre calado
233
+ if (/\bpython3?\s+-c\b/.test(s) && multi)
234
+ return 'python -c MULTILINHA falha em silêncio no Windows. Escreva um arquivo .py com escrever_arquivo e rode "python arquivo.py".';
235
+ if (/\bnode\s+-e\b/.test(s) && (multi || /\bimport\s*\(/.test(s) || /\bawait\b/.test(s)))
236
+ return 'node -e MULTILINHA ou com import()/await de topo falha no eval. Escreva um arquivo .mjs com escrever_arquivo e rode "node arquivo.mjs".';
237
+ // timeout /t é do cmd; sob git-bash pega o timeout do coreutils e dá "invalid time interval"
238
+ if (/\btimeout\s+\/t\b/i.test(s))
239
+ return 'Este shell é bash (não cmd): "timeout /t N" falha. Para esperar use "sleep N". Comandos só-do-cmd (dir, type, if exist) rode via: cmd /c "…".';
240
+ // matar processo por IMAGEM mata tudo com esse nome (inclusive a própria missão)
241
+ if (/\btaskkill\b[^\n]*\/im\b/i.test(s))
242
+ return 'taskkill /IM mata TODOS os processos com esse nome (inclusive os do usuário e possivelmente o próprio ts). Mate pelo PID da PORTA: netstat -ano | findstr :PORTA → taskkill /PID <pid> /F.';
243
+ return null;
244
+ }
245
+
227
246
  function selfDestructiveReason(cmd, opts = {}) {
228
247
  const c = String(cmd || '');
229
248
  if (!c.trim()) return null;
@@ -345,6 +364,11 @@ async function execute(name, input, opts = {}) {
345
364
  // RECUSA INSTANTÂNEA (defesa em profundidade — o gate do agent.js já pega antes):
346
365
  // comando auto-destrutivo NUNCA roda e NUNCA espera aprovação.
347
366
  { const sd = selfDestructiveReason(comando, { cwd: baseDir }); if (sd) return { erro: sd }; }
367
+ // GOTCHAS DE SHELL (B2/B6/B10): padrões que FALHAM de forma confusa no Windows.
368
+ // Barrar ANTES de rodar (com o conserto na mão) economiza o ciclo erro→tentativa→erro
369
+ // que o agente repetia: python -c multilinha morria em silêncio, node -e com import()
370
+ // quebrava no eval, e `timeout /t` (cmd) virava o timeout do coreutils sob git-bash.
371
+ { const g = _shellGotcha(comando); if (g) return { erro: g }; }
348
372
  // Comandos de INSTALL/BUILD (apt/npm/pip/make/gcc/docker build…) levam muito mais que 60s —
349
373
  // sem isso o apt-get de vários pacotes MORRIA no timeout padrão (nada instalava, em silêncio).
350
374
  // Eles ganham 10min de default (até 30min se o agente pedir timeout_s); resto segue 60s.
@@ -355,7 +379,9 @@ async function execute(name, input, opts = {}) {
355
379
  return await new Promise((res) => {
356
380
  require('child_process').exec(cmd, { timeout, shell: true, windowsHide: true, maxBuffer: 4 * 1024 * 1024, cwd: fs.existsSync(baseDir) ? baseDir : undefined }, (e, out, err) => {
357
381
  const c = compactor.compact(comando, out || '', { codigo: e?.code ?? 0 });
358
- const r = { stdout: c.out, stderr: compactor.stripAnsi(err || '').slice(0, 1500), codigo: e?.code ?? 0 };
382
+ // B18: mascara chave/token ANTES de virar contexto do modelo e linha de log.
383
+ const _rd = require('./core').redactSecrets;
384
+ const r = { stdout: _rd(c.out), stderr: _rd(compactor.stripAnsi(err || '').slice(0, 1500)), codigo: e?.code ?? 0 };
359
385
  if (c.note) r.aviso = c.note;
360
386
  if (!r.stdout.trim() && !r.stderr.trim() && r.codigo === 0 && /python3? -c/.test(comando) && comando.includes('\n'))
361
387
  r.aviso = 'stdout VAZIO: no Windows, python -c multi-linha falha em silêncio. ESCREVA um .py com escrever_arquivo e rode "python arquivo.py".';
@@ -392,7 +418,9 @@ async function execute(name, input, opts = {}) {
392
418
  const comando = String(input.comando || '');
393
419
  const out = await ssh.exec(comando, { timeoutMs: 180000 });
394
420
  const c = compactor.compact(comando, out.stdout || '', { codigo: out.code });
395
- const r = { remoto: (ssh.info() && ssh.info().host) || '', stdout: c.out, stderr: compactor.stripAnsi(out.stderr || '').slice(0, 1500), codigo: out.code };
421
+ // B18: foi AQUI que a chave da IA vazou (cat .env / grep na store de chaves na VPS).
422
+ const _rd = require('./core').redactSecrets;
423
+ const r = { remoto: (ssh.info() && ssh.info().host) || '', stdout: _rd(c.out), stderr: _rd(compactor.stripAnsi(out.stderr || '').slice(0, 1500)), codigo: out.code };
396
424
  if (c.note) r.aviso = c.note;
397
425
  return r;
398
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.92.0",
3
+ "version": "0.93.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"