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 +7 -6
- package/lib/agent.js +7 -25
- package/lib/core.js +76 -0
- package/lib/tools.js +3 -14
- package/package.json +4 -1
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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(
|
|
841
|
-
_emit({
|
|
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
|
-
// ──
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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",
|