terminal-smart-cli 0.32.0 → 0.33.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
@@ -747,6 +747,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
747
747
  if (task.length < 8) { console.error(ui.infoLine(T.agent_need)); process.exit(2); }
748
748
 
749
749
  const agent = require('../lib/agent');
750
+ const core = require('../lib/core'); // envelope de eventos canônico (stream-json)
750
751
  // --modelo/--model força o executor do agente (bake-off de modelos, ex: --modelo glm-5.2)
751
752
  const _mi = process.argv.findIndex(a => a === '--modelo' || a === '--model');
752
753
  const model = _mi >= 0 ? (process.argv[_mi + 1] || null) : null;
@@ -783,14 +784,14 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
783
784
  const ask = askFn || ui.ask;
784
785
  const sp = streamJson ? { text() {}, start() {}, stop() {} } : ui.spinner(T.agent_thinking).start();
785
786
  const t0 = Date.now();
786
- if (streamJson) _emit({ type: 'system', subtype: 'init', model: model || 'auto', cwd: startCwd, mode: plan ? 'plan' : readOnly ? 'ask' : 'agent' });
787
+ if (streamJson) _emit(core.AgentEvents.systemInit({ model: model || 'auto', cwd: startCwd, mode: plan ? 'plan' : readOnly ? 'ask' : 'agent' }));
787
788
  let out;
788
789
  try {
789
790
  out = await agent.run(task, {
790
791
  token, lang: cfg.lang || 'pt', yes: YES, model, priorMessages, cwd: startCwd, readOnly, plan,
791
792
  onThinking: () => sp.text(T.agent_thinking),
792
793
  onStep: ({ name, detail, blocked }) => {
793
- if (streamJson) { _emit({ type: 'tool', subtype: blocked ? 'blocked' : 'started', tool: name, detail: detail || '' }); return; }
794
+ if (streamJson) { _emit(core.AgentEvents.tool({ subtype: blocked ? 'blocked' : 'started', tool: name, detail: detail || '' })); return; }
794
795
  sp.stop();
795
796
  const tag = blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
796
797
  console.log(' ' + tag + ' ' + C.bold(name) + (detail ? C.dim(' · ' + detail) : ''));
@@ -798,14 +799,14 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
798
799
  },
799
800
  askApprove: async (cmd) => {
800
801
  // headless (stream-json): não dá pra perguntar → NEGA o destrutivo e sinaliza o evento.
801
- if (streamJson) { _emit({ type: 'tool', subtype: 'approval_denied', reason: 'headless (--stream-json): destructive command not auto-approved', command: cmd }); return false; }
802
+ if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_denied', reason: 'headless (--stream-json): destructive command not auto-approved', command: cmd })); return false; }
802
803
  sp.stop();
803
804
  const ans = String(await ask(C.err('▲ ') + T.agent_approve(C.bold(cmd)))).trim().toLowerCase();
804
805
  sp.start();
805
806
  return ['s', 'sim', 'y', 'yes'].includes(ans);
806
807
  },
807
808
  onRemote: ({ ttl }) => {
808
- if (streamJson) { _emit({ type: 'tool', subtype: 'approval_remote', ttl: ttl || 120 }); return; }
809
+ if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_remote', ttl: ttl || 120 })); return; }
809
810
  sp.stop();
810
811
  console.log(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120)));
811
812
  sp.start();
@@ -837,8 +838,8 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
837
838
  };
838
839
  // HEADLESS stream-json: fecha com assistant (texto) + result (métricas) e sai.
839
840
  if (streamJson) {
840
- _emit({ type: 'assistant', text: out.text || '' });
841
- _emit({ type: 'result', steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context || null, needHuman: out.needHuman || null, cwd: effCwd, duration_ms: Date.now() - t0, worktree: _wt ? { path: _wt.path, branch: _wt.branch, base: _wt.base, changed: (_wtInfo && _wtInfo.changed) || 0 } : null });
841
+ _emit(core.AgentEvents.assistant(out.text || ''));
842
+ _emit(core.AgentEvents.result({ steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context || null, needHuman: out.needHuman || null, cwd: effCwd, duration_ms: Date.now() - t0, worktree: _wt ? { path: _wt.path, branch: _wt.branch, base: _wt.base, changed: (_wtInfo && _wtInfo.changed) || 0 } : null }));
842
843
  return;
843
844
  }
844
845
  if (JSON_OUT) { console.log(JSON.stringify({ ok: true, result: out.text, steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context, needHuman: out.needHuman })); return; }
package/lib/agent.js CHANGED
@@ -32,32 +32,14 @@ function installedSkills() {
32
32
 
33
33
  const MAX_ITER = 15;
34
34
  const TOOL_RESULT_CAP = 6000; // chars por resultado no contexto (compactor já reduz antes)
35
- // Executor padrão do agente/missão: deepseek-v4-flash venceu o bake-off de código
36
- // (3-5x mais barato e eficiente que MiniMax/kimi/glm). O roteador 'smart' caía no
37
- // MiniMax caro; fixar aqui corta custo e melhora qualidade. --modelo sobrepõe.
38
- const DEFAULT_EXECUTOR = 'deepseek-v4-flash';
39
35
 
40
- // ── AUTO-COMPACTAÇÃO DE CONTEXTO ─────────────────────────────────────────────
41
- // Modelos degradam BEM antes do limite duro (e o kimi buga perto do dele), então
42
- // compactamos PROATIVO a ~65% da janela: o histórico antigo vira UM resumo denso
43
- // e as últimas mensagens ficam íntegras a conversa nunca "morre" por contexto.
44
- const MODEL_WINDOWS = {
45
- 'deepseek': 128000, 'glm-5.2': 200000, 'glm-4.7': 128000, 'kimi': 200000,
46
- 'minimax': 200000, 'qwen': 64000, 'gemini': 500000, 'mimo': 128000,
47
- };
48
- const CTX_WINDOW_DEFAULT = 100000; // modelo desconhecido → conservador
49
- // limiar/rabo têm override por env (TS_COMPACT_AT / TS_COMPACT_TAIL) — usado nos
50
- // testes pra forçar a compactação numa conversa curta, e por power users pra afinar.
51
- const COMPACT_AT = Number(process.env.TS_COMPACT_AT) > 0 ? Number(process.env.TS_COMPACT_AT) : 0.65;
52
- // piso de 6: rabo curto demais apaga a memória de trabalho e o agente relê arquivos em loop
53
- const KEEP_TAIL = Math.max(6, Number(process.env.TS_COMPACT_TAIL) > 0 ? Number(process.env.TS_COMPACT_TAIL) : 10);
54
- function winFor(model) {
55
- const m = String(model || DEFAULT_EXECUTOR).toLowerCase();
56
- for (const k of Object.keys(MODEL_WINDOWS)) if (m.includes(k)) return MODEL_WINDOWS[k];
57
- return CTX_WINDOW_DEFAULT;
58
- }
59
- // heurística chars/4 (mesma do compactor) — suficiente pra decidir QUANDO compactar
60
- const estMsgsTok = (msgs) => msgs.reduce((n, m) => n + Math.ceil(((typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')).length + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0)) / 4), 0);
36
+ // ── NÚCLEO CANÔNICO ──────────────────────────────────────────────────────────
37
+ // Executor padrão, janelas de contexto e auto-compactação vêm de lib/core.js (fonte
38
+ // única compartilhável). deepseek-v4-flash venceu o bake-off de código (3-5x mais
39
+ // barato); compactamos PROATIVO a ~65% da janela (o histórico antigo vira UM resumo
40
+ // denso e as últimas mensagens ficam íntegras) — a conversa nunca "morre" por contexto.
41
+ const core = require('./core');
42
+ const { DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok } = core;
61
43
 
62
44
  function systemPrompt(lang, cwd) {
63
45
  const pt = lang !== 'en';
package/lib/core.js ADDED
@@ -0,0 +1,76 @@
1
+ // lib/core.js — NÚCLEO CANÔNICO do motor de agente do Terminal Smart.
2
+ //
3
+ // Decisão de arquitetura (2026-07-18, auditoria de paridade dos 3 motores CLI/App/Web):
4
+ // o motor do CLI é o super-conjunto e vira o núcleo oficial. Este arquivo concentra as
5
+ // partes PURAS e SEM efeito colateral que hoje estavam reimplementadas de forma
6
+ // divergente no App (needsConfirm/DESTRUCTIVE_RE, _compactConvInPlace) e no Web
7
+ // (_scanDanger, histHot). O CLI é o primeiro a consumi-lo (dogfood); App e Web
8
+ // convergem sobre ele depois (via stream-json/ACP). Ver reference_ts_3_motores_paridade.
9
+ //
10
+ // REGRA: capacidade nova de agente (gate, compactação, roteamento, envelope de eventos)
11
+ // entra AQUI primeiro. Nada neste arquivo pode ter efeito colateral (I/O, rede, estado)
12
+ // — só funções puras e constantes, pra ser importável por qualquer superfície e testável.
13
+
14
+ // ── 1) GATE DE DESTRUTIVO ─────────────────────────────────────────────────────
15
+ // Padrões de comando destrutivo/irreversível — SEMPRE pedem aprovação humana.
16
+ // (fonte única: antes vivia em tools.js; App e Web tinham cópias divergentes.)
17
+ const DESTRUCTIVE = [
18
+ /\brm\s+(-[a-z]*[rf][a-z]*\s+)/i, /\brm\s+.*\*/, /\brmdir\b/i, /\bdel\s+\/[sq]/i, /\brd\s+\/s/i,
19
+ /\bmkfs\b/i, /\bdd\s+if=/i, /(?:^|[\s&;|])format\s+[a-z]:/i, /\bdiskpart\b/i, /> ?\/dev\/sd/i,
20
+ /\bshutdown\b/i, /\breboot\b/i, /\bhalt\b/i, /\bpoweroff\b/i,
21
+ /\bDROP\s+(TABLE|DATABASE)\b/i, /\bTRUNCATE\b/i, /\bDELETE\s+FROM\b[^;]*(;|$)(?![^]*WHERE)/i,
22
+ /\bchmod\s+(-R\s+)?777\b/i, /\bgit\s+push\s+.*--force(?!-with-lease)/i, /\bgit\s+reset\s+--hard/i,
23
+ /:\(\)\s*\{/, /\bcurl\b[^|]*\|\s*(sudo\s+)?(ba)?sh/i, /\bwget\b[^|]*\|\s*(sudo\s+)?(ba)?sh/i,
24
+ /\bkillall\b/i, /\bpkill\b/i, /\buserdel\b/i, /\bpasswd\b/i,
25
+ /\btaskkill\b.*\/im\b/i, /\btaskkill\b.*\/f\b.*\/im/i, // taskkill /IM mata TODOS os processos daquele nome (ex: node.exe = a própria missão)
26
+ /\bStop-Process\b[^|;&]*-Name\b/i, /\bGet-Process\b[^|]*\|[^|]*\bStop-Process\b/i, // PowerShell: matar por NOME mata todos (=taskkill /IM); Stop-Process -Id <pid> segue liberado
27
+ /\bsystemctl\s+(stop|disable|mask)\b/i, /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm)\b/i,
28
+ ];
29
+ function isDestructive(cmd) { return DESTRUCTIVE.some(re => re.test(String(cmd || ''))); }
30
+
31
+ // ── 2) JANELA DE CONTEXTO / AUTO-COMPACTAÇÃO ──────────────────────────────────
32
+ // Modelos degradam BEM antes do limite duro; compactamos PROATIVO a ~65% da janela.
33
+ const MODEL_WINDOWS = {
34
+ 'deepseek': 128000, 'glm-5.2': 200000, 'glm-4.7': 128000, 'kimi': 200000,
35
+ 'minimax': 200000, 'qwen': 64000, 'gemini': 500000, 'mimo': 128000,
36
+ };
37
+ const CTX_WINDOW_DEFAULT = 100000; // modelo desconhecido → conservador
38
+ const DEFAULT_EXECUTOR = 'deepseek-v4-flash'; // executor padrão (venceu o bake-off de código)
39
+ // limiar/rabo têm override por env (TS_COMPACT_AT / TS_COMPACT_TAIL) — usado nos testes.
40
+ const COMPACT_AT = Number(process.env.TS_COMPACT_AT) > 0 ? Number(process.env.TS_COMPACT_AT) : 0.65;
41
+ // piso de 6: rabo curto demais apaga a memória de trabalho e o agente relê arquivos em loop
42
+ const KEEP_TAIL = Math.max(6, Number(process.env.TS_COMPACT_TAIL) > 0 ? Number(process.env.TS_COMPACT_TAIL) : 10);
43
+ function winFor(model) {
44
+ const m = String(model || DEFAULT_EXECUTOR).toLowerCase();
45
+ for (const k of Object.keys(MODEL_WINDOWS)) if (m.includes(k)) return MODEL_WINDOWS[k];
46
+ return CTX_WINDOW_DEFAULT;
47
+ }
48
+ // heurística chars/4 — suficiente pra decidir QUANDO compactar
49
+ const estMsgsTok = (msgs) => msgs.reduce((n, m) => n + Math.ceil(((typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')).length + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0)) / 4), 0);
50
+
51
+ // ── 3) ENVELOPE DE EVENTOS (contrato interno único) ───────────────────────────
52
+ // Formaliza os eventos que o modo headless (stream-json) já emite. Este é o CONTRATO
53
+ // canônico: quando App (IPC ai:event) e Web (SSE) convergirem, mapeiam pra ESTAS formas.
54
+ // Tipos: system(init) · tool(started|blocked|approval_denied|approval_remote) · assistant · result.
55
+ const AgentEvents = {
56
+ systemInit: ({ model, cwd, mode }) => ({ type: 'system', subtype: 'init', model, cwd, mode }),
57
+ tool: ({ subtype, tool, detail, reason, command, ttl }) => {
58
+ const e = { type: 'tool', subtype };
59
+ if (tool !== undefined) e.tool = tool;
60
+ if (detail !== undefined) e.detail = detail;
61
+ if (reason !== undefined) e.reason = reason;
62
+ if (command !== undefined) e.command = command;
63
+ if (ttl !== undefined) e.ttl = ttl;
64
+ return e;
65
+ },
66
+ assistant: (text) => ({ type: 'assistant', text: text || '' }),
67
+ result: (fields) => Object.assign({ type: 'result' }, fields),
68
+ };
69
+ // nomes dos eventos, pra quem for validar/mapear (App/Web)
70
+ const EVENT_TYPES = ['system', 'tool', 'assistant', 'result'];
71
+
72
+ module.exports = {
73
+ DESTRUCTIVE, isDestructive,
74
+ MODEL_WINDOWS, CTX_WINDOW_DEFAULT, DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok,
75
+ AgentEvents, EVENT_TYPES,
76
+ };
package/lib/tools.js CHANGED
@@ -34,20 +34,9 @@ function _snapshot(absPath) {
34
34
  } catch (_) { return null; }
35
35
  }
36
36
 
37
- // Padrões de comando destrutivo/irreversível SEMPRE pedem aprovação humana.
38
- const DESTRUCTIVE = [
39
- /\brm\s+(-[a-z]*[rf][a-z]*\s+)/i, /\brm\s+.*\*/, /\brmdir\b/i, /\bdel\s+\/[sq]/i, /\brd\s+\/s/i,
40
- /\bmkfs\b/i, /\bdd\s+if=/i, /(?:^|[\s&;|])format\s+[a-z]:/i, /\bdiskpart\b/i, /> ?\/dev\/sd/i,
41
- /\bshutdown\b/i, /\breboot\b/i, /\bhalt\b/i, /\bpoweroff\b/i,
42
- /\bDROP\s+(TABLE|DATABASE)\b/i, /\bTRUNCATE\b/i, /\bDELETE\s+FROM\b[^;]*(;|$)(?![^]*WHERE)/i,
43
- /\bchmod\s+(-R\s+)?777\b/i, /\bgit\s+push\s+.*--force(?!-with-lease)/i, /\bgit\s+reset\s+--hard/i,
44
- /:\(\)\s*\{/, /\bcurl\b[^|]*\|\s*(sudo\s+)?(ba)?sh/i, /\bwget\b[^|]*\|\s*(sudo\s+)?(ba)?sh/i,
45
- /\bkillall\b/i, /\bpkill\b/i, /\buserdel\b/i, /\bpasswd\b/i,
46
- /\btaskkill\b.*\/im\b/i, /\btaskkill\b.*\/f\b.*\/im/i, // taskkill /IM mata TODOS os processos daquele nome (ex: node.exe = a própria missão)
47
- /\bStop-Process\b[^|;&]*-Name\b/i, /\bGet-Process\b[^|]*\|[^|]*\bStop-Process\b/i, // PowerShell: matar por NOME mata todos (=taskkill /IM); Stop-Process -Id <pid> segue liberado
48
- /\bsystemctl\s+(stop|disable|mask)\b/i, /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm)\b/i,
49
- ];
50
- function isDestructive(cmd) { return DESTRUCTIVE.some(re => re.test(String(cmd || ''))); }
37
+ // Gate de destrutivo: fonte ÚNICA no núcleo canônico (lib/core.js). Reexportado aqui
38
+ // pra compatibilidade (tools.isDestructive segue funcionando pra quem já importava).
39
+ const { isDestructive } = require('./core');
51
40
 
52
41
  // Definições no formato OpenAI function-calling — schemas SIMPLES de propósito
53
42
  // (Gemini via CDC rejeita propertyNames/additionalProperties; só type/properties/required).
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.32.0",
3
+ "version": "0.33.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"
7
7
  },
8
+ "scripts": {
9
+ "test": "node test/core.test.js"
10
+ },
8
11
  "files": [
9
12
  "bin",
10
13
  "lib",