terminal-smart-cli 0.80.0 → 0.82.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 +12 -5
- package/lib/agent.js +5 -1
- package/lib/i18n.js +2 -2
- package/lib/ui.js +69 -11
- package/package.json +1 -1
package/bin/ts.js
CHANGED
|
@@ -195,9 +195,12 @@ async function sendMessage(token, content) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
function printAnswer(text, ms) {
|
|
198
|
+
const en = cfg.lang === 'en';
|
|
199
|
+
const w = Math.min(process.stdout.columns || 80, 96);
|
|
198
200
|
console.log('');
|
|
199
|
-
|
|
200
|
-
console.log(
|
|
201
|
+
// rótulo de quem fala, cor INDIGO (o usuário é ciano) — fica claro onde o TS começa
|
|
202
|
+
console.log(' ' + C.indigo(C.bold(en ? 'Terminal Smart' : 'Terminal Smart')) + C.dim(':'));
|
|
203
|
+
console.log(ui.md(text, w - 2).split('\n').map(l => ' ' + l).join('\n'));
|
|
201
204
|
if (ms) console.log(' ' + C.dim('· ' + (ms / 1000).toFixed(1).replace('.', ',') + 's'));
|
|
202
205
|
console.log('');
|
|
203
206
|
}
|
|
@@ -518,7 +521,9 @@ async function chatRepl() {
|
|
|
518
521
|
let closed = false, pending = null;
|
|
519
522
|
rl.on('close', () => { closed = true; if (pending) { const p = pending; pending = null; p(null); } });
|
|
520
523
|
const ask1 = (q) => new Promise((res) => { pending = res; rl.question(q, (a) => { pending = null; res(a); }); });
|
|
521
|
-
|
|
524
|
+
// rótulo do usuário em CIANO (o Terminal Smart responde em indigo) — cores distintas deixam
|
|
525
|
+
// claro onde começa cada fala.
|
|
526
|
+
const prompt = C.cyan(C.bold(T.chat_prompt));
|
|
522
527
|
// cwd da SESSÃO do agente: persiste entre mensagens (o problema 1). "cd"/"pwd"
|
|
523
528
|
// no REPL alteram/mostram; cada msg roteada pro agente resolve caminhos daqui.
|
|
524
529
|
const _pp = require('path'), _oo = require('os'), _ff = require('fs');
|
|
@@ -887,8 +892,10 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
|
|
|
887
892
|
onStep: ({ name, detail, blocked, loop, retry, auto }) => {
|
|
888
893
|
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : auto ? 'auto_approved' : 'started', tool: name, detail: detail || '' })); return; }
|
|
889
894
|
sp.stop();
|
|
890
|
-
const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('
|
|
891
|
-
|
|
895
|
+
const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('›');
|
|
896
|
+
// nome AMIGÁVEL (Rodando no servidor…) em vez do técnico (executar_remoto); detalhe sem cortar palavra
|
|
897
|
+
const _label = ui.friendlyTool(name, cfg.lang === 'en');
|
|
898
|
+
console.log(' ' + tag + ' ' + C.bold(_label) + (detail ? C.dim(' · ' + ui.wordTrunc(detail, 60)) : ''));
|
|
892
899
|
sp.start();
|
|
893
900
|
},
|
|
894
901
|
onStepDone: (e) => {
|
package/lib/agent.js
CHANGED
|
@@ -67,7 +67,9 @@ REGRAS:
|
|
|
67
67
|
- PRIVILÉGIO MÍNIMO em infra: pra dar acesso de uma aplicação a um serviço (banco, broker), crie uma CREDENCIAL DEDICADA com o mínimo de permissão (ex: CREATE USER 'app' + GRANT só no schema necessário) e aponte a app pra ela. NUNCA altere a autenticação/config GLOBAL existente (root/admin) só pra uma app conectar — isso afeta TUDO que já usa aquele acesso (backups, outros serviços, seu próprio acesso). Se precisar MESMO mudar algo global, AVISE explicitamente o que mais será afetado e por quê.
|
|
68
68
|
- NUNCA peça segredo ao usuário: jamais peça senha/chave/token/credencial no chat. Se uma operação remota (ssh/scp) falhar por autenticação, REUSE a conexão/chave que JÁ funcionou nesta sessão (mesmo host/user/chave — não reconstrua o comando do zero esquecendo o -i da chave). Se ainda assim não conectar, peça pro usuário EXECUTAR a ação — nunca entregar o segredo.
|
|
69
69
|
- NÃO DESISTA sem TENTAR: é PROIBIDO responder "não tenho acesso" / "não consigo" / "preciso que você me passe X" enquanto houver uma ferramenta que você ainda não usou pra tentar. Antes de declarar que algo é impossível, AJA: conecte (conectar_vps), procure (buscar_arquivos, ou grep/find via executar_comando/executar_remoto), leia (ler_arquivo). Ex.: pediram pra ler um código-fonte que "você não tem"? Se há uma VPS/pasta onde ele pode estar, CONECTE e procure (grep -rn "<símbolo>" <dir>) ANTES de dizer que não tem. Só afirme que não conseguiu DEPOIS de ter tentado de fato e mostre o erro/saída REAL que te barrou.
|
|
70
|
+
- NUNCA pergunte "onde está o arquivo X?" / "há um diretório com Y?" / "posso gerar Z?" sem ANTES ter PROCURADO de fato: rode find/buscar_arquivos por ele em TODOS os lugares plausíveis — na VPS conectada (find / -name "arquivo" 2>/dev/null, e nas pastas do projeto) E na máquina local do usuário (buscar_arquivos, incluindo o pack/instalação de origem que ele citou). Se um config/arquivo obrigatório faltar mesmo depois de procurar, tente ACHAR um exemplo/modelo (outro .conf parecido, o default no código-fonte) e GERAR a partir dele — só peça ajuda ao usuário como ÚLTIMO recurso, dizendo exatamente onde já procurou e não achou.
|
|
70
71
|
- 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.
|
|
72
|
+
- 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.
|
|
71
73
|
- Termine SEMPRE com um resumo curto: o que foi feito, resultado e caminhos de arquivos criados/alterados.
|
|
72
74
|
- Responda no idioma do usuário (padrão: português do Brasil).`
|
|
73
75
|
: 'You are the Terminal Smart agent running ON THIS machine via CLI, with REAL tools. Act for real — never say "I will do it" without calling the tool in the same turn, and never invent results that did not come from a tool.'
|
|
@@ -89,7 +91,9 @@ RULES:
|
|
|
89
91
|
- LEAST PRIVILEGE in infra: to give an app access to a service (DB, broker), create a DEDICATED credential with minimal permission (e.g. CREATE USER 'app' + GRANT on the needed schema only) and point the app at it. NEVER alter the existing GLOBAL auth/config (root/admin) just to make an app connect — that affects EVERYTHING already using that access (backups, other services, your own access). If you truly must change something global, explicitly WARN what else will be affected and why.
|
|
90
92
|
- NEVER ask the user for a secret: never ask for a password/key/token/credential in chat. If a remote op (ssh/scp) fails on auth, REUSE the connection/key that ALREADY worked this session (same host/user/key — don't rebuild the command dropping the -i key flag). If it still won't connect, ask the user to PERFORM the action — never to hand over the secret.
|
|
91
93
|
- DON'T GIVE UP without TRYING: it is FORBIDDEN to answer "I don't have access" / "I can't" / "I need you to give me X" while there's a tool you haven't used yet to try. Before declaring something impossible, ACT: connect (conectar_vps), search (buscar_arquivos, or grep/find via executar_comando/executar_remoto), read (ler_arquivo). E.g. asked to read source code you "don't have"? If there's a VPS/folder where it might live, CONNECT and search (grep -rn "<symbol>" <dir>) BEFORE saying you don't have it. Only claim you couldn't do it AFTER actually trying, and show the REAL error/output that blocked you.
|
|
94
|
+
- NEVER ask "where is file X?" / "is there a folder with Y?" / "may I generate Z?" without having actually SEARCHED first: run find/buscar_arquivos for it in EVERY plausible place — on the connected VPS (find / -name "file" 2>/dev/null, and the project folders) AND on the user's local machine (buscar_arquivos, including the source pack/install they mentioned). If a required config/file is still missing after searching, try to FIND a template/example (a similar .conf, the default in the source) and GENERATE from it — only ask the user as a LAST resort, stating exactly where you already looked and didn't find it.
|
|
92
95
|
- 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.
|
|
96
|
+
- 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.
|
|
93
97
|
- ALWAYS end with a short summary: what was done, the result, and paths of created/changed files.
|
|
94
98
|
- Answer in the user's language.`);
|
|
95
99
|
const volatile = `\n\nMáquina: ${process.platform} ${os.release()} · host ${os.hostname()} · user ${os.userInfo().username}`
|
|
@@ -134,7 +138,7 @@ async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools =
|
|
|
134
138
|
// argsShort: resumo de 1 linha do input da ferramenta pro passo exibido no terminal
|
|
135
139
|
function argsShort(name, input) {
|
|
136
140
|
const v = input.comando || input.caminho || input.padrao || input.tarefa || input.slug || '';
|
|
137
|
-
return String(v).replace(/\s+/g, ' ').slice(0,
|
|
141
|
+
return String(v).replace(/\s+/g, ' ').slice(0, 200); // corte fino (sem partir palavra) é feito na UI com wordTrunc
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
// ── LOOP-DETECTION (anti-trava) ──────────────────────────────────────────────
|
package/lib/i18n.js
CHANGED
|
@@ -139,7 +139,7 @@ const STR = {
|
|
|
139
139
|
no_credits: 'Seus créditos acabaram. Gerencie em terminalsmart.com.br/planos',
|
|
140
140
|
ask_empty: 'Diga o que você precisa. Ex.: ts "como liberar a porta 443 no ufw?"',
|
|
141
141
|
chat_hint: 'digite sua mensagem · "sair" encerra · "/nova" zera o contexto',
|
|
142
|
-
chat_prompt: '
|
|
142
|
+
chat_prompt: 'Você › ',
|
|
143
143
|
chat_bye: 'Até logo! A conversa continua salva na web (/ia).',
|
|
144
144
|
agent_need: 'Descreva a tarefa. Ex.: ts agente "libere espaço em disco analisando as pastas maiores"',
|
|
145
145
|
agent_thinking: 'agente pensando',
|
|
@@ -291,7 +291,7 @@ const STR = {
|
|
|
291
291
|
no_credits: 'You are out of credits. Manage at terminalsmart.com.br/planos',
|
|
292
292
|
ask_empty: 'Tell me what you need. E.g.: ts "how do I open port 443 on ufw?"',
|
|
293
293
|
chat_hint: 'type your message · "exit" quits · "/new" resets context',
|
|
294
|
-
chat_prompt: '
|
|
294
|
+
chat_prompt: 'You › ',
|
|
295
295
|
chat_bye: 'See you! The conversation stays saved on the web (/ia).',
|
|
296
296
|
agent_need: 'Describe the task. E.g.: ts agente "free disk space by analyzing the largest folders"',
|
|
297
297
|
agent_thinking: 'agent thinking',
|
package/lib/ui.js
CHANGED
|
@@ -62,28 +62,86 @@ function spinner(initial) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Quebra UMA linha de texto por PALAVRA na largura w — nunca parte palavra no meio
|
|
66
|
+
// (pedido do user: se não coube, a palavra inteira desce). Preserva o recuo/bullet na 1ª sub-linha.
|
|
67
|
+
function _wrapLine(line, w) {
|
|
68
|
+
if (line.length <= w) return [line];
|
|
69
|
+
const lead = (line.match(/^\s*(?:[-*]\s+)?/) || [''])[0]; // recuo + bullet só na 1ª
|
|
70
|
+
const cont = ' '.repeat(lead.length);
|
|
71
|
+
const words = line.slice(lead.length).split(/\s+/).filter(Boolean);
|
|
72
|
+
const rows = []; let cur = lead;
|
|
73
|
+
for (const wd of words) {
|
|
74
|
+
const atStart = (cur === lead || cur === cont);
|
|
75
|
+
if (!atStart && (cur.length + 1 + wd.length) > w) { rows.push(cur); cur = cont + wd; }
|
|
76
|
+
else cur = atStart ? cur + wd : cur + ' ' + wd;
|
|
77
|
+
}
|
|
78
|
+
rows.push(cur);
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
// Markdown-lite: negrito, código inline, blocos de código com borda, títulos, listas.
|
|
66
|
-
|
|
83
|
+
// wrapW = largura pra word-wrap do texto (não quebra dentro de bloco de código).
|
|
84
|
+
function md(text, wrapW) {
|
|
67
85
|
if (!TTY) return String(text);
|
|
86
|
+
const w = Math.max(24, (wrapW || _width()) - 2);
|
|
68
87
|
const out = [];
|
|
69
88
|
const lines = String(text).split('\n');
|
|
70
89
|
let inCode = false;
|
|
71
90
|
for (const line of lines) {
|
|
72
91
|
if (/^\s*```/.test(line)) { inCode = !inCode; out.push(C.dim(' ' + (inCode ? '╭──' : '╰──'))); continue; }
|
|
73
92
|
if (inCode) { out.push(C.dim(' │ ') + C.cyan(line)); continue; }
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
// word-wrap ANTES de colorir (o ANSI de negrito/código não muda a largura visível)
|
|
94
|
+
for (let l of _wrapLine(line, w)) {
|
|
95
|
+
l = l.replace(/\[CMD\]\s*([\s\S]*?)\s*\[\/CMD\]/g, (_, c) => C.cyan('$ ' + c));
|
|
96
|
+
l = l.replace(/\[\/?CMD\]/g, '');
|
|
97
|
+
l = l.replace(/^#{1,3}\s+(.+)$/, (_, h) => C.bold(C.cyan(h)));
|
|
98
|
+
l = l.replace(/\*\*([^*]+)\*\*/g, (_, b) => C.bold(b));
|
|
99
|
+
l = l.replace(/`([^`]+)`/g, (_, c) => C.cyan(c));
|
|
100
|
+
l = l.replace(/^(\s*)[-*]\s+/, (_, sp) => sp + C.cyan('•') + ' ');
|
|
101
|
+
out.push(l);
|
|
102
|
+
}
|
|
83
103
|
}
|
|
84
104
|
return out.join('\n');
|
|
85
105
|
}
|
|
86
106
|
|
|
107
|
+
// Trunca sem CORTAR palavra: se passar de n, volta ao último espaço + reticências.
|
|
108
|
+
function wordTrunc(s, n = 64) {
|
|
109
|
+
const str = String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
|
|
110
|
+
if (str.length <= n) return str;
|
|
111
|
+
const cut = str.slice(0, n);
|
|
112
|
+
const sp = cut.lastIndexOf(' ');
|
|
113
|
+
// volta ao último espaço (não parte palavra); só corta cru se a 1ª palavra já passa de n.
|
|
114
|
+
return (sp > 0 ? cut.slice(0, sp) : cut).replace(/\s+$/, '') + '…';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Nome da ferramenta em LINGUAGEM NATURAL (o que ela FAZ) — some com "executar_remoto" e cia.
|
|
118
|
+
const _TOOL_PT = {
|
|
119
|
+
executar_comando: 'Rodando comando', executar_remoto: 'Rodando no servidor',
|
|
120
|
+
ler_arquivo: 'Lendo arquivo', escrever_arquivo: 'Criando arquivo', editar_arquivo: 'Editando arquivo',
|
|
121
|
+
listar_diretorio: 'Listando a pasta', buscar_arquivos: 'Procurando arquivos', buscar_codigo: 'Buscando no código',
|
|
122
|
+
mapa_projeto: 'Mapeando o projeto', mudar_diretorio: 'Entrando na pasta', info_sistema: 'Checando o sistema',
|
|
123
|
+
conectar_vps: 'Conectando no servidor', enviar_arquivo: 'Enviando arquivo', buscar_web: 'Pesquisando na web',
|
|
124
|
+
navegador: 'Navegando na web', explorar: 'Explorando o projeto', lembrar: 'Salvando na memória',
|
|
125
|
+
skill_gerenciar: 'Ajustando uma habilidade', compactar_contexto: 'Resumindo o histórico',
|
|
126
|
+
pedir_ajuda_humano: 'Pedindo sua ajuda', ler_planilha: 'Lendo a planilha',
|
|
127
|
+
};
|
|
128
|
+
const _TOOL_EN = {
|
|
129
|
+
executar_comando: 'Running command', executar_remoto: 'Running on the server',
|
|
130
|
+
ler_arquivo: 'Reading file', escrever_arquivo: 'Writing file', editar_arquivo: 'Editing file',
|
|
131
|
+
listar_diretorio: 'Listing folder', buscar_arquivos: 'Finding files', buscar_codigo: 'Searching the code',
|
|
132
|
+
mapa_projeto: 'Mapping the project', mudar_diretorio: 'Entering folder', info_sistema: 'Checking the system',
|
|
133
|
+
conectar_vps: 'Connecting to the server', enviar_arquivo: 'Uploading file', buscar_web: 'Searching the web',
|
|
134
|
+
navegador: 'Browsing the web', explorar: 'Exploring the project', lembrar: 'Saving to memory',
|
|
135
|
+
skill_gerenciar: 'Tuning a skill', compactar_contexto: 'Summarizing history',
|
|
136
|
+
pedir_ajuda_humano: 'Asking for your help', ler_planilha: 'Reading the spreadsheet',
|
|
137
|
+
};
|
|
138
|
+
function friendlyTool(name, en) {
|
|
139
|
+
const map = en ? _TOOL_EN : _TOOL_PT;
|
|
140
|
+
if (map[name]) return map[name];
|
|
141
|
+
if (/^mcp_/.test(name)) return (en ? 'External tool' : 'Ferramenta externa');
|
|
142
|
+
return String(name || '').replace(/_/g, ' '); // fallback legível
|
|
143
|
+
}
|
|
144
|
+
|
|
87
145
|
// Barra de progresso ▰▰▰▱▱ (uso de créditos)
|
|
88
146
|
function bar(frac, width = 22) {
|
|
89
147
|
const n = Math.round(Math.max(0, Math.min(1, frac)) * width);
|
|
@@ -115,4 +173,4 @@ const okLine = (s) => ' ' + C.ok('✔') + ' ' + s;
|
|
|
115
173
|
const errLine = (s) => ' ' + C.err('✖') + ' ' + s;
|
|
116
174
|
const infoLine = (s) => ' ' + C.cyan('⌁') + ' ' + s;
|
|
117
175
|
|
|
118
|
-
module.exports = { C, TTY, gradient, banner, box, spinner, md, bar, ask, cmdSummary, okLine, errLine, infoLine };
|
|
176
|
+
module.exports = { C, TTY, gradient, banner, box, spinner, md, bar, ask, cmdSummary, okLine, errLine, infoLine, wordTrunc, friendlyTool, _wrapLine };
|