terminal-smart-cli 0.32.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/README.md +55 -0
- package/bin/ts.js +1267 -0
- package/lib/acp.js +191 -0
- package/lib/agent.js +470 -0
- package/lib/api.js +77 -0
- package/lib/compactor.js +152 -0
- package/lib/config.js +20 -0
- package/lib/eval.js +243 -0
- package/lib/hooks.js +72 -0
- package/lib/i18n.js +294 -0
- package/lib/memoria.js +38 -0
- package/lib/meta.js +1199 -0
- package/lib/router.js +82 -0
- package/lib/skills.js +81 -0
- package/lib/ssh.js +93 -0
- package/lib/tools.js +397 -0
- package/lib/ui.js +106 -0
- package/package.json +35 -0
package/lib/router.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Roteador de intenção — o coração do "conversa 100% natural": o usuário digita
|
|
2
|
+
// qualquer coisa e o sistema decide o destino. Em CAMADAS pra custar quase nada:
|
|
3
|
+
// 1) heurística determinística (zero tokens, resolve ~85-90%)
|
|
4
|
+
// 2) classificador flash-lite SÓ na dúvida (centavos — mesmo padrão do web-intent)
|
|
5
|
+
// Errar é barato de propósito: agente tem gate de destrutivo, orquestração tem
|
|
6
|
+
// aprovação de plano com custo — o roteador nunca dispara gasto grande sozinho.
|
|
7
|
+
const { api } = require('./api');
|
|
8
|
+
|
|
9
|
+
// Pergunta/conversa: forma interrogativa ou pedido de explicação
|
|
10
|
+
const Q_RE = /^(como|por ?qu[eê]|o ?que|qual|quais|quando|onde|quem|ser[aá] que|devo|posso|vale a pena|existe|tem como|é melhor|what|how|why|which|when|where|explique|me explica|explica|o que significa|diferen[çc]a entre)\b|\?\s*$/i;
|
|
11
|
+
|
|
12
|
+
// Verbos de AÇÃO (imperativo) — quem pede ação quer execução, não aula
|
|
13
|
+
const ACT_RE = /\b(cri[ae]|criar|instal[ae]|execut[ae]|rod[ae]|verifi(que|car)|cheque|confira|liber[ae]|limp[ae]|corrij[ae]|consert[ae]|configur[ae]|atualiz[ae]|apag[ue]|remov[ae]|delet[ae]|mov[ae]|renomei[ae]|salv[ae]|baix[ae]|ger[ae]|fa[çc]a|mont[ae]|compil[ae]|test[ae]|reinici[ae]|par[ae] o|sub[ae]|otimiz[ae]|organiz[ae]|analis[ae]|list[ae]|mostr[ae]|abr[ae]|zip[ae]|compact[ae]|extrai[ae]|clon[ae]|comit[ae]|create|install|run|check|fix|clean|delete|remove|generate|make|build|list|show)\b/i;
|
|
14
|
+
|
|
15
|
+
// Alvos LOCAIS/de sistema — sinal de "faça NESTA máquina"
|
|
16
|
+
const LOCAL_RE = /\b(disco|espa[çc]o|pasta|diret[óo]rio|arquivo|servi[çc]o|processo|porta|mem[óo]ria|cpu|logs?|docker|container|nginx|apache|git|npm|pip|python|node|backup|temp|downloads?|desktop|aqui|nesta m[áa]quina|meu (pc|computador|servidor|not(e|ebook))|vps|sistema|firewall|cron|zip|script|\.txt|\.js|\.py|\.json|\.env|\.log|c:\\|\/var\/|\/etc\/|\/opt\/|\/home\/)\b/i;
|
|
17
|
+
|
|
18
|
+
// Construção GRANDE multi-entregável (mesma família do _looksBigTask do app)
|
|
19
|
+
const BUILD_RE = /\b(cri[ae]|mont[ae]|desenvolv[ae]|constru[aá]|fa[çc]a (um|uma)|ger[ae]|implement[ae]|prepar[ae]|escrev[ae])\b/i;
|
|
20
|
+
const BIG_RE = /\b(completo|sistema|site|aplicativo|app|kit|painel|plataforma|loja|portal|api|dashboard|landing|portf[óo]lio|apresenta[çc][ãa]o|plano de neg[óo]cio|campanha|e-?book)\b/i;
|
|
21
|
+
|
|
22
|
+
function heuristic(msg) {
|
|
23
|
+
const t = String(msg || '').trim();
|
|
24
|
+
if (!t) return 'chat';
|
|
25
|
+
const multi = t.length > 150 || (t.match(/,/g) || []).length >= 2 || (t.match(/\d\)/g) || []).length >= 2;
|
|
26
|
+
const isBigBuild = BUILD_RE.test(t) && BIG_RE.test(t) && multi;
|
|
27
|
+
// construção grande COM alvo local (pasta/servidor/arquivo) → agente faz AQUI;
|
|
28
|
+
// sem alvo local → orquestração na nuvem (plano + custo + aprovação)
|
|
29
|
+
if (isBigBuild) return LOCAL_RE.test(t) ? 'agente' : 'run';
|
|
30
|
+
// pergunta clara SEM imperativo no começo → conversa
|
|
31
|
+
if (Q_RE.test(t) && !/^(cri[ae]|instal[ae]|execut[ae]|rod[ae]|fa[çc]a|ger[ae]|mont[ae]|apag[ue]|remov[ae]|corrij[ae]|limp[ae]|liber[ae])\b/i.test(t)) return 'chat';
|
|
32
|
+
// ação + alvo local → agente
|
|
33
|
+
if (ACT_RE.test(t) && LOCAL_RE.test(t)) return 'agente';
|
|
34
|
+
// imperativo logo no começo → agente mesmo sem alvo explícito
|
|
35
|
+
if (/^(cri[ae]|criar|instal[ae]|execut[ae]|rod[ae]|roda|fa[çc]a|ger[ae]|mont[ae]|salv[ae]|baix[ae]|list[ae]|verifi(que|car)|cheque|limp[ae]|corrij[ae]|configur[ae]|atualiz[ae]|test[ae]|analis[ae]|otimiz[ae]|organiz[ae]|mostr[ae]|abr[ae])\b/i.test(t)) return 'agente';
|
|
36
|
+
return null; // ambíguo → classificador decide
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let _keyCache = null;
|
|
40
|
+
async function _aiKey(token) {
|
|
41
|
+
if (_keyCache) return _keyCache;
|
|
42
|
+
_keyCache = await api('/api/ai/key', { token, timeoutMs: 15000 });
|
|
43
|
+
return _keyCache;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Classificador flash-lite (só na dúvida). Falhou/estranho → 'chat' (comportamento de hoje).
|
|
47
|
+
async function classify(msg, token) {
|
|
48
|
+
try {
|
|
49
|
+
const k = await _aiKey(token);
|
|
50
|
+
const ctrl = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => ctrl.abort(), 12000);
|
|
52
|
+
const res = await fetch(String(k.baseUrl).replace(/\/+$/, '') + '/chat/completions', {
|
|
53
|
+
method: 'POST', signal: ctrl.signal,
|
|
54
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + k.key },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
model: 'gemini-2.5-flash-lite', stream: false, max_completion_tokens: 5,
|
|
57
|
+
messages: [
|
|
58
|
+
{ role: 'system', content: 'Classifique a intenção da mensagem de um usuário de um CLI DevOps. Responda APENAS uma palavra: "chat" (pergunta, conversa, pedido de explicação), "agente" (quer que algo seja FEITO nesta máquina: comandos, arquivos, diagnóstico, instalação) ou "orquestrar" (objetivo GRANDE com vários entregáveis para uma equipe de agentes).' },
|
|
59
|
+
{ role: 'user', content: String(msg).slice(0, 500) },
|
|
60
|
+
],
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
const j = await res.json().catch(() => null);
|
|
65
|
+
const word = String(j?.choices?.[0]?.message?.content || '').toLowerCase();
|
|
66
|
+
// cobra a classificação (mesmo padrão do web-intent) — best-effort
|
|
67
|
+
const u = j?.usage || {};
|
|
68
|
+
if (u.prompt_tokens) api('/api/credit/charge', { method: 'POST', token, body: { model: 'gemini-2.5-flash-lite', inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }).catch(() => {});
|
|
69
|
+
if (word.includes('agente')) return 'agente';
|
|
70
|
+
if (word.includes('orquestrar')) return 'run';
|
|
71
|
+
return 'chat';
|
|
72
|
+
} catch (_) { return 'chat'; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// API principal: destino + por qual camada decidiu (pra linha de transparência)
|
|
76
|
+
async function route(msg, token) {
|
|
77
|
+
const h = heuristic(msg);
|
|
78
|
+
if (h) return { dest: h, via: 'heuristica' };
|
|
79
|
+
return { dest: await classify(msg, token), via: 'ia' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { route, heuristic };
|
package/lib/skills.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Galeria de SKILLS da comunidade (MVP): publicar/descobrir/instalar skills via o backend
|
|
2
|
+
// do ts (terminalsmart.com.br /api/skills). Uma skill = pasta com SKILL.md (frontmatter
|
|
3
|
+
// name/description/category + corpo = instruções) + opcional script.(sh|js) + references/.
|
|
4
|
+
// Instalada em ~/.ts/skills/<slug>/. Transparência: o `add` mostra o conteúdo ANTES de gravar.
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { api } = require('./api');
|
|
9
|
+
|
|
10
|
+
const SKILLS_DIR = path.join(os.homedir(), '.ts', 'skills');
|
|
11
|
+
|
|
12
|
+
async function list(token, q) {
|
|
13
|
+
const qs = q ? ('?q=' + encodeURIComponent(q)) : '';
|
|
14
|
+
const r = await api('/api/skills' + qs, { token, timeoutMs: 20000 });
|
|
15
|
+
return (r && r.skills) || [];
|
|
16
|
+
}
|
|
17
|
+
async function get(token, slug) {
|
|
18
|
+
const r = await api('/api/skills/' + encodeURIComponent(slug), { token, timeoutMs: 20000 });
|
|
19
|
+
return r && r.skill;
|
|
20
|
+
}
|
|
21
|
+
async function publish(token, body) {
|
|
22
|
+
return api('/api/skills', { method: 'POST', token, body, timeoutMs: 30000 });
|
|
23
|
+
}
|
|
24
|
+
function markInstalled(token, slug) {
|
|
25
|
+
return api('/api/skills/' + encodeURIComponent(slug) + '/install', { method: 'POST', token, timeoutMs: 15000 }).catch(() => {});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Lê uma skill LOCAL (pasta com SKILL.md OU um .md solto) → { name, description, category, content }.
|
|
29
|
+
// content = { instructions, script:{nome,conteudo}|null, references:[{nome,conteudo}] }
|
|
30
|
+
function readLocal(dir) {
|
|
31
|
+
let mdPath = dir, base = dir;
|
|
32
|
+
const st = fs.statSync(dir);
|
|
33
|
+
if (st.isDirectory()) { mdPath = path.join(dir, 'SKILL.md'); base = dir; }
|
|
34
|
+
else { base = path.dirname(dir); }
|
|
35
|
+
if (!fs.existsSync(mdPath)) throw new Error('SKILL.md não encontrado em ' + dir);
|
|
36
|
+
const raw = fs.readFileSync(mdPath, 'utf8');
|
|
37
|
+
// frontmatter YAML simples entre --- ... ---
|
|
38
|
+
const meta = { name: '', description: '', category: 'geral' };
|
|
39
|
+
let bodyStr = raw;
|
|
40
|
+
const fm = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
41
|
+
if (fm) {
|
|
42
|
+
bodyStr = fm[2];
|
|
43
|
+
for (const line of fm[1].split('\n')) {
|
|
44
|
+
const m = line.match(/^(name|description|category|título|titulo)\s*:\s*(.+)$/i);
|
|
45
|
+
if (m) { const k = m[1].toLowerCase(); meta[k === 'título' || k === 'titulo' ? 'name' : k] = m[2].trim().replace(/^["']|["']$/g, ''); }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!meta.name) meta.name = path.basename(base);
|
|
49
|
+
// script opcional
|
|
50
|
+
let script = null;
|
|
51
|
+
for (const s of ['script.js', 'script.sh', 'run.js', 'run.sh']) {
|
|
52
|
+
const p = path.join(fs.statSync(dir).isDirectory() ? dir : base, s);
|
|
53
|
+
if (fs.existsSync(p)) { script = { nome: s, conteudo: fs.readFileSync(p, 'utf8').slice(0, 40000) }; break; }
|
|
54
|
+
}
|
|
55
|
+
// references/ opcional
|
|
56
|
+
const references = [];
|
|
57
|
+
const refDir = path.join(fs.statSync(dir).isDirectory() ? dir : base, 'references');
|
|
58
|
+
if (fs.existsSync(refDir)) {
|
|
59
|
+
for (const f of fs.readdirSync(refDir).slice(0, 20)) {
|
|
60
|
+
try { const fp = path.join(refDir, f); if (fs.statSync(fp).isFile()) references.push({ nome: f, conteudo: fs.readFileSync(fp, 'utf8').slice(0, 40000) }); } catch (_) {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { name: meta.name, description: meta.description, category: meta.category, content: { instructions: bodyStr.trim().slice(0, 40000), script, references } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Grava uma skill baixada em ~/.ts/skills/<slug>/ (SKILL.md + script + references).
|
|
67
|
+
function writeLocal(skill) {
|
|
68
|
+
const dir = path.join(SKILLS_DIR, skill.slug);
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
const c = skill.content || {};
|
|
71
|
+
const fm = `---\nname: ${skill.name}\ndescription: ${skill.description || ''}\ncategory: ${skill.category || 'geral'}\nautor: ${skill.author || ''}\n---\n\n`;
|
|
72
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), fm + (c.instructions || ''), 'utf8');
|
|
73
|
+
if (c.script && c.script.nome) fs.writeFileSync(path.join(dir, c.script.nome), c.script.conteudo || '', 'utf8');
|
|
74
|
+
if (Array.isArray(c.references) && c.references.length) {
|
|
75
|
+
const rd = path.join(dir, 'references'); fs.mkdirSync(rd, { recursive: true });
|
|
76
|
+
for (const r of c.references) { if (r && r.nome) fs.writeFileSync(path.join(rd, path.basename(r.nome)), r.conteudo || '', 'utf8'); }
|
|
77
|
+
}
|
|
78
|
+
return dir;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { list, get, publish, markInstalled, readLocal, writeLocal, SKILLS_DIR };
|
package/lib/ssh.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Conexão SSH do ts — permite o agente (e o comando `ts vps`) LOGAR num servidor
|
|
2
|
+
// remoto e rodar comandos lá, da máquina local. Usa ssh2 (senha OU chave privada).
|
|
3
|
+
// Mantém UMA conexão viva por processo (stateful) pra o agente reusar entre passos.
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
let _conn = null; // cliente ssh2 conectado
|
|
9
|
+
let _info = null; // { host, port, user }
|
|
10
|
+
let Client = null;
|
|
11
|
+
try { Client = require('ssh2').Client; } catch (_) { /* ssh2 ausente → erro amigável no connect */ }
|
|
12
|
+
|
|
13
|
+
// Perfil salvo em ~/.ts/config.json { vps: { host, port, user, pass, keyPath } }
|
|
14
|
+
function _cfgFile() { return path.join(os.homedir(), '.ts', 'config.json'); }
|
|
15
|
+
function loadProfile() {
|
|
16
|
+
try { const c = JSON.parse(fs.readFileSync(_cfgFile(), 'utf8')); return c.vps || null; } catch (_) { return null; }
|
|
17
|
+
}
|
|
18
|
+
function saveProfile(vps) {
|
|
19
|
+
const f = _cfgFile();
|
|
20
|
+
let c = {}; try { c = JSON.parse(fs.readFileSync(f, 'utf8')); } catch (_) {}
|
|
21
|
+
c.vps = vps;
|
|
22
|
+
try { fs.mkdirSync(path.dirname(f), { recursive: true }); } catch (_) {}
|
|
23
|
+
fs.writeFileSync(f, JSON.stringify(c, null, 2));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Lê um arquivo de login (formato flexível): 1 linha = só senha; 2 linhas = usuário + senha;
|
|
27
|
+
// ou linhas "user:", "senha:", "host:" rotuladas. Retorna {user, pass, host}.
|
|
28
|
+
function parseLoginFile(p) {
|
|
29
|
+
const t = fs.readFileSync(p, 'utf8');
|
|
30
|
+
const lines = t.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const l of lines) {
|
|
33
|
+
const m = l.match(/^(usu[aá]rio|user|login|senha|password|pass|host|ip|servidor)\s*[:=]\s*(.+)$/i);
|
|
34
|
+
if (m) {
|
|
35
|
+
const k = m[1].toLowerCase();
|
|
36
|
+
if (/usu|user|login/.test(k)) out.user = m[2].trim();
|
|
37
|
+
else if (/senha|pass/.test(k)) out.pass = m[2].trim();
|
|
38
|
+
else out.host = m[2].trim();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!out.user && !out.pass) {
|
|
42
|
+
if (lines.length === 1) out.pass = lines[0];
|
|
43
|
+
else if (lines.length >= 2) { out.user = lines[0]; out.pass = lines[1]; }
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isConnected() { return !!_conn; }
|
|
49
|
+
function info() { return _info; }
|
|
50
|
+
|
|
51
|
+
function connect(opts = {}) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
if (!Client) return reject(new Error('Biblioteca ssh2 não instalada no CLI.'));
|
|
54
|
+
const prof = loadProfile() || {};
|
|
55
|
+
const host = opts.host || prof.host;
|
|
56
|
+
const port = opts.port || prof.port || 22;
|
|
57
|
+
const user = opts.user || prof.user || 'root';
|
|
58
|
+
let password = opts.pass || prof.pass;
|
|
59
|
+
let privateKey;
|
|
60
|
+
const keyPath = opts.keyPath || prof.keyPath;
|
|
61
|
+
if (keyPath) { try { privateKey = fs.readFileSync(keyPath); } catch (_) {} }
|
|
62
|
+
if (!host) return reject(new Error('Sem host — configure com "ts vps set" ou passe --host.'));
|
|
63
|
+
if (!password && !privateKey) return reject(new Error('Sem senha nem chave — configure a credencial.'));
|
|
64
|
+
|
|
65
|
+
if (_conn) { try { _conn.end(); } catch (_) {} _conn = null; }
|
|
66
|
+
const c = new Client();
|
|
67
|
+
const cfg = { host, port, username: user, readyTimeout: 20000, keepaliveInterval: 15000 };
|
|
68
|
+
if (privateKey) cfg.privateKey = privateKey; else cfg.password = password;
|
|
69
|
+
c.on('ready', () => { _conn = c; _info = { host, port, user }; resolve({ host, port, user }); });
|
|
70
|
+
c.on('error', (e) => { _conn = null; reject(new Error('Falha ao conectar: ' + (e && e.message || e))); });
|
|
71
|
+
c.on('close', () => { if (_conn === c) { _conn = null; _info = null; } });
|
|
72
|
+
try { c.connect(cfg); } catch (e) { reject(e); }
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Roda um comando no servidor conectado. Retorna {code, stdout, stderr}.
|
|
77
|
+
function exec(cmd, { timeoutMs = 120000 } = {}) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
if (!_conn) return reject(new Error('Não conectado. Rode "ts vps" ou conectar_vps primeiro.'));
|
|
80
|
+
_conn.exec(cmd, (err, stream) => {
|
|
81
|
+
if (err) return reject(err);
|
|
82
|
+
let stdout = '', stderr = '', done = false;
|
|
83
|
+
const to = setTimeout(() => { if (!done) { done = true; try { stream.close(); } catch (_) {} resolve({ code: 124, stdout, stderr: stderr + '\n[timeout]' }); } }, timeoutMs);
|
|
84
|
+
stream.on('close', (code) => { if (done) return; done = true; clearTimeout(to); resolve({ code: code == null ? 0 : code, stdout, stderr }); });
|
|
85
|
+
stream.on('data', (d) => { stdout += d.toString(); if (stdout.length > 200000) stdout = stdout.slice(-120000); });
|
|
86
|
+
stream.stderr.on('data', (d) => { stderr += d.toString(); if (stderr.length > 60000) stderr = stderr.slice(-40000); });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function disconnect() { if (_conn) { try { _conn.end(); } catch (_) {} _conn = null; _info = null; return true; } return false; }
|
|
92
|
+
|
|
93
|
+
module.exports = { connect, exec, disconnect, isConnected, info, loadProfile, saveProfile, parseLoginFile };
|