terminal-smart-cli 0.98.0 → 0.99.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 +7 -5
- package/bin/ts.js +344 -196
- package/hub/cli.js +255 -0
- package/hub/controle.js +262 -0
- package/hub/dpapi.js +55 -0
- package/hub/estado.js +274 -0
- package/hub/exportar-telas.js +149 -0
- package/hub/hub.js +717 -0
- package/hub/mascara.js +67 -0
- package/hub/parar.js +151 -0
- package/hub/principal.js +5 -0
- package/hub/proto/PROTOCOLO.md +789 -0
- package/hub/proto/ts-proto.js +1643 -0
- package/hub/relay-conexao.js +200 -0
- package/hub/telas-arquivo.js +130 -0
- package/hub/tmux.js +189 -0
- package/hub/vps/README.md +109 -0
- package/hub/vps/tmux-agentes.conf +5 -0
- package/hub/vps/ts-hub-exportar-telas.service +30 -0
- package/hub/vps/ts-hub-parar-consumir +67 -0
- package/hub/vps/ts-hub-parar.path +16 -0
- package/hub/vps/ts-hub-parar.service +23 -0
- package/hub/vps/ts-hub-parar.tmpfiles.conf +10 -0
- package/hub/vps/ts-hub.service +39 -0
- package/hub/vps/ts-tmux.service +19 -0
- package/lib/acp.js +8 -2
- package/lib/agent.js +186 -87
- package/lib/chat-local.js +139 -0
- package/lib/core.js +336 -23
- package/lib/doctor.js +13 -5
- package/lib/i18n.js +69 -31
- package/lib/ia-local.js +388 -0
- package/lib/intelligence-core.js +108 -1
- package/lib/keyring.js +0 -1
- package/lib/meta.js +8 -8
- package/lib/personal-agent-llm.js +99 -26
- package/lib/sentinela.js +2 -2
- package/lib/tools.js +638 -8
- package/package.json +4 -2
package/hub/mascara.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Máscara de segredos nas telas de terminal, antes de saírem da máquina (exportar-telas) e de
|
|
3
|
+
// novo no hub (defesa em profundidade). É uma rede de segurança, não uma garantia: segredo
|
|
4
|
+
// em formato desconhecido passa. Por isso a lista de janelas exportadas é fechada.
|
|
5
|
+
//
|
|
6
|
+
// Mascara: chaves sk-… / sk-ant-…, tokens do GitHub (ghp_/gho_/ghu_/ghs_/ghr_/github_pat_),
|
|
7
|
+
// chaves da AWS (AKIA…/ASIA…), tokens do Slack (xox…), JWT (eyJ….….…), blocos
|
|
8
|
+
// "-----BEGIN … PRIVATE KEY-----" e senha em URL (usuario:senha@).
|
|
9
|
+
//
|
|
10
|
+
// A tela vem com escapes de cor (SGR) que podem cortar um segredo ao meio. Cada linha é
|
|
11
|
+
// conferida também sem as cores; se o segredo só aparece sem as cores, a linha sai sem cor.
|
|
12
|
+
|
|
13
|
+
const RX_SGR = /\x1b\[[0-9;:]*m/g;
|
|
14
|
+
const MARCA = '[mascarado]';
|
|
15
|
+
|
|
16
|
+
const PADROES = [
|
|
17
|
+
[/sk-ant-[A-Za-z0-9_-]{8,}/g, 'sk-ant-' + MARCA],
|
|
18
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, 'sk-' + MARCA],
|
|
19
|
+
[/\bgithub_pat_[A-Za-z0-9_]{20,}/g, 'github_pat_' + MARCA],
|
|
20
|
+
[/\b(gh[pousr]_)[A-Za-z0-9]{20,}/g, '$1' + MARCA],
|
|
21
|
+
[/\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, '$1' + MARCA],
|
|
22
|
+
[/\b(xox[abposr]-)[A-Za-z0-9-]{10,}/g, '$1' + MARCA],
|
|
23
|
+
[/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g, 'jwt-' + MARCA],
|
|
24
|
+
// esquema://usuario:senha@host → esquema://usuario:[mascarado]@host
|
|
25
|
+
[/\b([a-z][a-z0-9+.-]{0,30}:\/\/[^\s/:@]{1,256}):[^\s/@]{1,256}@/gi, '$1:' + MARCA + '@'],
|
|
26
|
+
];
|
|
27
|
+
const RX_INICIO_CHAVE = /-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----/;
|
|
28
|
+
const RX_FIM_CHAVE = /-----END [A-Z0-9 ]{0,40}PRIVATE KEY-----/;
|
|
29
|
+
|
|
30
|
+
function semCor(s) { return s.replace(RX_SGR, ''); }
|
|
31
|
+
|
|
32
|
+
function mascararLinha(linha) {
|
|
33
|
+
let s = linha;
|
|
34
|
+
for (const [rx, troca] of PADROES) s = s.replace(rx, troca);
|
|
35
|
+
return s;
|
|
36
|
+
}
|
|
37
|
+
function temSegredo(linha) {
|
|
38
|
+
return PADROES.some(([rx]) => { rx.lastIndex = 0; const r = rx.test(linha); rx.lastIndex = 0; return r; });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Devolve a tela com os segredos conhecidos trocados por "[mascarado]". */
|
|
42
|
+
function mascararSegredos(texto) {
|
|
43
|
+
const linhas = String(texto).split('\n');
|
|
44
|
+
let dentroDeChave = false;
|
|
45
|
+
for (let i = 0; i < linhas.length; i++) {
|
|
46
|
+
const original = linhas[i];
|
|
47
|
+
const plana = semCor(original);
|
|
48
|
+
if (dentroDeChave) {
|
|
49
|
+
if (RX_FIM_CHAVE.test(plana)) dentroDeChave = false;
|
|
50
|
+
linhas[i] = RX_FIM_CHAVE.test(plana) ? '-----END PRIVATE KEY-----' : MARCA;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (RX_INICIO_CHAVE.test(plana)) {
|
|
54
|
+
// O bloco inteiro some, até o END (ou até o fim da tela, se o END não estiver nela).
|
|
55
|
+
linhas[i] = plana.slice(0, plana.search(RX_INICIO_CHAVE)) + '-----BEGIN PRIVATE KEY----- ' + MARCA;
|
|
56
|
+
dentroDeChave = !RX_FIM_CHAVE.test(plana.slice(plana.search(RX_INICIO_CHAVE) + 10));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
let m = mascararLinha(original);
|
|
60
|
+
// Segredo partido por escapes de cor: só aparece no texto sem cor. Sai a linha sem cor.
|
|
61
|
+
if (temSegredo(semCor(m))) m = mascararLinha(plana);
|
|
62
|
+
linhas[i] = m;
|
|
63
|
+
}
|
|
64
|
+
return linhas.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { mascararSegredos, MARCA };
|
package/hub/parar.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// "Parar tudo" com confirmação real (entrega 1, revisão de segurança).
|
|
3
|
+
//
|
|
4
|
+
// O hub não para nada sozinho: ele grava um PEDIDO num diretório fixo e espera um consumidor
|
|
5
|
+
// externo (na VPS: unidade systemd de root, exemplos em cli/hub/vps/) gravar a CONFIRMAÇÃO.
|
|
6
|
+
// Só com a confirmação o hub responde { parado:true } ao celular; sem ela, no prazo,
|
|
7
|
+
// E_PARAR_SEM_CONFIRMACAO. hub.status/hub.estado refletem a confirmação, nunca o pedido.
|
|
8
|
+
//
|
|
9
|
+
// <TS_HUB_PARAR_DIR>/
|
|
10
|
+
// pedidos/ dono = usuário do hub, sem escrita para grupo/outros. O hub cria aqui
|
|
11
|
+
// parar-<carimbo ms>-<16 hex>.pedido com O_EXCL (+ O_NOFOLLOW).
|
|
12
|
+
// confirmacoes/ dono = o consumidor (root, uid 0 por padrão), sem escrita para
|
|
13
|
+
// grupo/outros. O consumidor grava parar-<...>.ok (ou .falhou).
|
|
14
|
+
//
|
|
15
|
+
// O consumidor não lê o conteúdo do pedido (só o nome, validado) e o renomeia para fora de
|
|
16
|
+
// pedidos/, então o .path do systemd não dispara em laço. "Religar" = o root apagar os .ok.
|
|
17
|
+
// Nó sem TS_HUB_PARAR_DIR (ex.: o PC na entrega 1) anuncia pararDisponivel:false.
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
const PEDIDOS = 'pedidos';
|
|
24
|
+
const CONFIRMACOES = 'confirmacoes';
|
|
25
|
+
const RX_BASE = /^parar-\d{13}-[0-9a-f]{16}$/;
|
|
26
|
+
const RX_OK = /^parar-\d{13}-[0-9a-f]{16}\.ok$/;
|
|
27
|
+
const PRAZO_PADRAO_MS = 10000;
|
|
28
|
+
const CONFIRMACOES_MAX_LIDAS = 200;
|
|
29
|
+
|
|
30
|
+
function erro(codigo) { return Object.assign(new Error(codigo), { codigo }); }
|
|
31
|
+
const dormir = ms => new Promise(r => setTimeout(r, ms));
|
|
32
|
+
|
|
33
|
+
class Parada {
|
|
34
|
+
/**
|
|
35
|
+
* opcoes: { dir (TS_HUB_PARAR_DIR ou null), prazoMs (10 s), sondaMs (100 ms),
|
|
36
|
+
* donoUid (dono exigido de confirmacoes/: 0 = root), plataforma, agora }
|
|
37
|
+
*/
|
|
38
|
+
constructor(opcoes) {
|
|
39
|
+
const o = opcoes || {};
|
|
40
|
+
this.dir = o.dir ? path.resolve(String(o.dir)) : null;
|
|
41
|
+
this.prazoMs = o.prazoMs || PRAZO_PADRAO_MS;
|
|
42
|
+
this.sondaMs = o.sondaMs || 100;
|
|
43
|
+
this.plataforma = o.plataforma || process.platform;
|
|
44
|
+
this.posix = this.plataforma !== 'win32';
|
|
45
|
+
this.donoUid = Number.isSafeInteger(o.donoUid) ? o.donoUid : 0;
|
|
46
|
+
this.meuUid = this.posix && typeof process.getuid === 'function' ? process.getuid() : null;
|
|
47
|
+
this.agora = o.agora || Date.now;
|
|
48
|
+
this._emCurso = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get configurado() { return !!this.dir; }
|
|
52
|
+
get dirPedidos() { return this.dir && path.join(this.dir, PEDIDOS); }
|
|
53
|
+
get dirConfirmacoes() { return this.dir && path.join(this.dir, CONFIRMACOES); }
|
|
54
|
+
|
|
55
|
+
/** Diretório real (sem link), sem escrita para grupo/outros e com o dono esperado. */
|
|
56
|
+
_dirSeguro(dir, dono) {
|
|
57
|
+
let st;
|
|
58
|
+
try { st = fs.lstatSync(dir); } catch (_) { return false; }
|
|
59
|
+
if (st.isSymbolicLink() || !st.isDirectory()) return false;
|
|
60
|
+
if (this.posix) {
|
|
61
|
+
if ((st.mode & 0o022) !== 0) return false;
|
|
62
|
+
if (dono !== null && st.uid !== dono) return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Há consumidor configurado e os diretórios estão no lugar, com dono e modo certos. */
|
|
68
|
+
disponivel() {
|
|
69
|
+
if (!this.dir) return false;
|
|
70
|
+
return this._dirSeguro(this.dir, null)
|
|
71
|
+
&& this._dirSeguro(this.dirPedidos, this.meuUid)
|
|
72
|
+
&& this._dirSeguro(this.dirConfirmacoes, this.posix ? this.donoUid : null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Arquivo de confirmação válido: regular, não link, dono = consumidor. */
|
|
76
|
+
_confirmacaoValida(arq) {
|
|
77
|
+
let st;
|
|
78
|
+
try { st = fs.lstatSync(arq); } catch (_) { return false; }
|
|
79
|
+
if (st.isSymbolicLink() || !st.isFile()) return false;
|
|
80
|
+
if (this.posix && st.uid !== this.donoUid) return false;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** O nó está parado = existe confirmação válida (não basta o pedido). */
|
|
85
|
+
parado() {
|
|
86
|
+
if (!this.dir || !this._dirSeguro(this.dirConfirmacoes, this.posix ? this.donoUid : null)) return false;
|
|
87
|
+
let nomes;
|
|
88
|
+
try { nomes = fs.readdirSync(this.dirConfirmacoes); } catch (_) { return false; }
|
|
89
|
+
let lidas = 0;
|
|
90
|
+
for (const n of nomes) {
|
|
91
|
+
if (!RX_OK.test(n)) continue;
|
|
92
|
+
if (++lidas > CONFIRMACOES_MAX_LIDAS) break;
|
|
93
|
+
if (this._confirmacaoValida(path.join(this.dirConfirmacoes, n))) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Grava o pedido sem seguir link e sem sobrescrever nada. Devolve o nome-base. */
|
|
99
|
+
_gravarPedido() {
|
|
100
|
+
const base = 'parar-' + String(this.agora()).padStart(13, '0').slice(-13) + '-' + crypto.randomBytes(8).toString('hex');
|
|
101
|
+
if (!RX_BASE.test(base)) throw erro('E_INTERNO');
|
|
102
|
+
const c = fs.constants;
|
|
103
|
+
const arq = path.join(this.dirPedidos, base + '.pedido');
|
|
104
|
+
// O nome tem 64 bits aleatórios (ninguém planta um link nele de antemão). Ainda assim:
|
|
105
|
+
// nada pode existir com esse nome (nem link quebrado) — no Windows não há O_NOFOLLOW e o
|
|
106
|
+
// CREATE_NEW seguiria um link plantado.
|
|
107
|
+
let existe = true;
|
|
108
|
+
try { fs.lstatSync(arq); } catch (e) { existe = !(e && e.code === 'ENOENT'); }
|
|
109
|
+
if (existe) throw erro('E_PARAR_INDISPONIVEL');
|
|
110
|
+
const flags = c.O_WRONLY | c.O_CREAT | c.O_EXCL | (c.O_NOFOLLOW || 0);
|
|
111
|
+
const fd = fs.openSync(arq, flags, 0o600);
|
|
112
|
+
try {
|
|
113
|
+
// O consumidor não lê isto; serve só para quem olhar o diretório à mão.
|
|
114
|
+
fs.writeSync(fd, JSON.stringify({ v: 1, por: 'ts hub', origem: 'celular', em: new Date(this.agora()).toISOString() }) + '\n');
|
|
115
|
+
} finally { fs.closeSync(fd); }
|
|
116
|
+
return base;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Pede a parada e espera a confirmação. Resolve { pedido } com .ok; rejeita com
|
|
121
|
+
* E_PARAR_INDISPONIVEL (sem consumidor) ou E_PARAR_SEM_CONFIRMACAO (prazo ou .falhou).
|
|
122
|
+
* Pedidos simultâneos (dois celulares) compartilham a mesma espera.
|
|
123
|
+
*/
|
|
124
|
+
pedir() {
|
|
125
|
+
if (this._emCurso) return this._emCurso;
|
|
126
|
+
const p = this._pedir();
|
|
127
|
+
this._emCurso = p;
|
|
128
|
+
const limpar = () => { if (this._emCurso === p) this._emCurso = null; };
|
|
129
|
+
p.then(limpar, limpar);
|
|
130
|
+
return p;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async _pedir() {
|
|
134
|
+
if (!this.disponivel()) throw erro('E_PARAR_INDISPONIVEL');
|
|
135
|
+
let base;
|
|
136
|
+
try { base = this._gravarPedido(); } catch (e) {
|
|
137
|
+
throw erro(e && e.codigo ? e.codigo : 'E_PARAR_INDISPONIVEL');
|
|
138
|
+
}
|
|
139
|
+
const ok = path.join(this.dirConfirmacoes, base + '.ok');
|
|
140
|
+
const falhou = path.join(this.dirConfirmacoes, base + '.falhou');
|
|
141
|
+
const limite = Date.now() + this.prazoMs;
|
|
142
|
+
for (;;) {
|
|
143
|
+
if (this._confirmacaoValida(ok)) return { pedido: base };
|
|
144
|
+
if (this._confirmacaoValida(falhou)) throw erro('E_PARAR_SEM_CONFIRMACAO');
|
|
145
|
+
if (Date.now() >= limite) throw erro('E_PARAR_SEM_CONFIRMACAO');
|
|
146
|
+
await dormir(Math.min(this.sondaMs, Math.max(1, limite - Date.now())));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { Parada, PEDIDOS, CONFIRMACOES, RX_BASE, PRAZO_PADRAO_MS };
|