terminal-smart-cli 0.74.0 → 0.76.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 +63 -14
- package/lib/agent.js +60 -14
- package/lib/core.js +24 -7
- package/lib/i18n.js +2 -0
- package/lib/meta.js +22 -14
- package/lib/ui.js +13 -1
- package/package.json +1 -1
package/bin/ts.js
CHANGED
|
@@ -19,12 +19,54 @@ const FLAGS = new Set(rawArgs.filter(a => a.startsWith('-')));
|
|
|
19
19
|
const POS = rawArgs.filter(a => !a.startsWith('-'));
|
|
20
20
|
const JSON_OUT = FLAGS.has('--json');
|
|
21
21
|
const YES = FLAGS.has('--yes') || FLAGS.has('-y');
|
|
22
|
+
// --yolo / --full-auto: modo FULL-AUTO (autoridade total) — aprova TUDO nesta sessão, inclusive
|
|
23
|
+
// destrutivos, sem perguntar. NÃO reusa --auto (já é "noturno" no meta e "detectar stack" no init).
|
|
24
|
+
// O gate anti-catástrofe de AUTO-destrutivos (rm -rf ~, mexer em ~/.ts…) segue RECUSANDO na hora
|
|
25
|
+
// mesmo aqui: full-auto não abre mão da proteção que mataria a própria máquina/missão.
|
|
26
|
+
const YOLO = FLAGS.has('--yolo') || FLAGS.has('--full-auto');
|
|
22
27
|
|
|
23
28
|
let cfg = config.load();
|
|
24
29
|
let T = t(cfg.lang || 'pt');
|
|
25
30
|
const { C } = ui;
|
|
26
31
|
|
|
27
32
|
function fmtK(n) { return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
|
|
33
|
+
|
|
34
|
+
// ── APROVAÇÃO DE PASSO (UX) ──────────────────────────────────────────────────
|
|
35
|
+
// askDestructive: prompt de comando DESTRUTIVO. Mostra um RESUMO curto em AMARELO (destaque,
|
|
36
|
+
// não vermelho = não é erro) — a 1ª linha do comando + quantas linhas ficam escondidas (num
|
|
37
|
+
// heredoc SQL a 1ª linha é o comando externo, ex "mysql -u root pw <<'SQL'"). Aceita:
|
|
38
|
+
// s/N = sim/não neste passo · a = aprovar TUDO na sessão (full-auto) · v = ver o comando inteiro
|
|
39
|
+
// Retorna true | false | 'all'.
|
|
40
|
+
async function askDestructive(cmd, { en = false } = {}) {
|
|
41
|
+
const raw = String(cmd || '');
|
|
42
|
+
const { shown, hidden } = ui.cmdSummary(raw);
|
|
43
|
+
const more = hidden > 0 ? C.dim(` … +${hidden} ${en ? 'lines' : 'linhas'}`) : '';
|
|
44
|
+
for (;;) {
|
|
45
|
+
const q = C.warn(' ▲ ' + (en ? 'destructive: ' : 'destrutivo: ')) + C.bold(shown) + more
|
|
46
|
+
+ C.dim(en ? ' [y/N · a=all · v=view] ' : ' [s/N · a=tudo · v=ver] ');
|
|
47
|
+
const a = String(await ui.ask(q)).trim().toLowerCase();
|
|
48
|
+
if (['v', 'ver', 'view'].includes(a)) { console.log('\n' + raw.split('\n').map(l => ' ' + C.dim(l)).join('\n') + '\n'); continue; }
|
|
49
|
+
if (['a', 'all', 'tudo'].includes(a)) return 'all';
|
|
50
|
+
return ['s', 'sim', 'y', 'yes'].includes(a);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// askLabelApprove: aprovação de um LABEL já formatado (skill/MCP, não é comando cru). s/N + a=tudo.
|
|
54
|
+
async function askLabelApprove(label, { en = false } = {}) {
|
|
55
|
+
const a = String(await ui.ask(C.warn('▲ ') + T.agent_approve(C.bold(label)) + C.dim(en ? '(a=all) ' : '(a=tudo) '))).trim().toLowerCase();
|
|
56
|
+
if (['a', 'all', 'tudo'].includes(a)) return 'all';
|
|
57
|
+
return ['s', 'sim', 'y', 'yes'].includes(a);
|
|
58
|
+
}
|
|
59
|
+
// Roteia o payload do askApprove do agente (obj destrutivo → resumo; string label → skill/MCP).
|
|
60
|
+
async function askApproveRoute(payload) {
|
|
61
|
+
const en = cfg.lang === 'en';
|
|
62
|
+
const isObj = payload && typeof payload === 'object';
|
|
63
|
+
const cmd = isObj ? payload.cmd : String(payload);
|
|
64
|
+
return (isObj && payload.kind === 'destructive') ? askDestructive(cmd, { en }) : askLabelApprove(cmd, { en });
|
|
65
|
+
}
|
|
66
|
+
// Linha de STATUS por etapa (✓ verde / ✗ vermelho) sob a linha ⚙, com recuo `pad`.
|
|
67
|
+
function stepDoneLine({ ok, evidence }, pad = ' ') {
|
|
68
|
+
return pad + (ok ? C.ok('✓') : C.err('✗')) + (evidence ? ' ' + C.dim(String(evidence).slice(0, 80)) : C.dim(ok ? ' ok' : ' erro'));
|
|
69
|
+
}
|
|
28
70
|
function needToken() {
|
|
29
71
|
if (cfg.token) return cfg.token;
|
|
30
72
|
console.error(ui.errLine(T.need_login));
|
|
@@ -824,22 +866,28 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
|
|
|
824
866
|
let out;
|
|
825
867
|
try {
|
|
826
868
|
out = await agent.run(task, {
|
|
827
|
-
token, lang: cfg.lang || 'pt', yes: YES, model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser,
|
|
869
|
+
token, lang: cfg.lang || 'pt', yes: YES, autoAll: YOLO, model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser,
|
|
828
870
|
onThinking: () => sp.text(T.agent_thinking),
|
|
829
|
-
onStep: ({ name, detail, blocked, loop, retry }) => {
|
|
830
|
-
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : 'started', tool: name, detail: detail || '' })); return; }
|
|
871
|
+
onStep: ({ name, detail, blocked, loop, retry, auto }) => {
|
|
872
|
+
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : auto ? 'auto_approved' : 'started', tool: name, detail: detail || '' })); return; }
|
|
831
873
|
sp.stop();
|
|
832
|
-
const tag = retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
|
|
874
|
+
const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
|
|
833
875
|
console.log(' ' + tag + ' ' + C.bold(name) + (detail ? C.dim(' · ' + detail) : ''));
|
|
834
876
|
sp.start();
|
|
835
877
|
},
|
|
836
|
-
|
|
878
|
+
onStepDone: (e) => {
|
|
879
|
+
if (streamJson) return; // o evento 'result' já cobre; aqui é só a UI bonita
|
|
880
|
+
sp.stop();
|
|
881
|
+
console.log(stepDoneLine(e, ' '));
|
|
882
|
+
sp.start();
|
|
883
|
+
},
|
|
884
|
+
askApprove: async (payload) => {
|
|
837
885
|
// headless (stream-json): não dá pra perguntar → NEGA o destrutivo e sinaliza o evento.
|
|
838
|
-
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_denied', reason: 'headless (--stream-json): destructive command not auto-approved', command: cmd })); return false; }
|
|
886
|
+
if (streamJson) { const cmd = payload && typeof payload === 'object' ? payload.cmd : payload; _emit(core.AgentEvents.tool({ subtype: 'approval_denied', reason: 'headless (--stream-json): destructive command not auto-approved', command: cmd })); return false; }
|
|
839
887
|
sp.stop();
|
|
840
|
-
const
|
|
888
|
+
const r = await askApproveRoute(payload);
|
|
841
889
|
sp.start();
|
|
842
|
-
return
|
|
890
|
+
return r;
|
|
843
891
|
},
|
|
844
892
|
onRemote: ({ ttl }) => {
|
|
845
893
|
if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_remote', ttl: ttl || 120 })); return; }
|
|
@@ -1276,7 +1324,7 @@ async function metaCmd() {
|
|
|
1276
1324
|
const goalArg = janela === 1 ? (goal || null) : null; // janelas seguintes só retomam
|
|
1277
1325
|
try {
|
|
1278
1326
|
st = await metaMod.run(goalArg, {
|
|
1279
|
-
token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria, prove,
|
|
1327
|
+
token, lang: cfg.lang || 'pt', yes, autoAll: YOLO, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria, prove,
|
|
1280
1328
|
onAlert: ({ type, text }) => {
|
|
1281
1329
|
sp.stop();
|
|
1282
1330
|
const ic = type === 'human' ? C.warn('🙋') : type === 'escalate' ? C.indigo('🧠') : type === 'stagnated' ? C.err('🛑') : type === 'design' ? C.cyan('🎨') : type === 'retry' || type === 'conn' ? C.warn('📡') : C.warn('⏱');
|
|
@@ -1290,12 +1338,13 @@ async function metaCmd() {
|
|
|
1290
1338
|
onChecklist: (itens) => { sp.stop(); console.log(_metaChecklistBox(itens) + '\n'); sp.start(); },
|
|
1291
1339
|
onRound: ({ n, item, attempt }) => { sp.stop(); console.log(' ' + C.indigo('◆') + ' ' + C.bold(T.meta_round(n, item.slice(0, 70), attempt))); sp.start(); },
|
|
1292
1340
|
onThinking: () => sp.text(T.agent_thinking),
|
|
1293
|
-
onStep: ({ name, detail, blocked, loop, retry }) => {
|
|
1341
|
+
onStep: ({ name, detail, blocked, loop, retry, auto }) => {
|
|
1294
1342
|
sp.stop();
|
|
1295
|
-
console.log(' ' + (retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙')) + ' ' + name + (detail ? C.dim(' · ' + detail) : ''));
|
|
1343
|
+
console.log(' ' + (auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙')) + ' ' + name + (detail ? C.dim(' · ' + detail) : ''));
|
|
1296
1344
|
sp.start();
|
|
1297
1345
|
},
|
|
1298
|
-
|
|
1346
|
+
onStepDone: (e) => { sp.stop(); console.log(stepDoneLine(e, ' ')); sp.start(); },
|
|
1347
|
+
askApprove: async (payload) => { sp.stop(); const r = await askApproveRoute(payload); sp.start(); return r; },
|
|
1299
1348
|
onRemote: ({ ttl }) => { sp.stop(); console.log(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120))); sp.start(); },
|
|
1300
1349
|
onRoundDone: ({ checklist, spent }) => {
|
|
1301
1350
|
sp.stop();
|
|
@@ -1812,8 +1861,8 @@ async function runbookCmd(args) {
|
|
|
1812
1861
|
cwd: process.cwd(),
|
|
1813
1862
|
askApprove: async (cmd) => {
|
|
1814
1863
|
if (YES || !process.stdin.isTTY) return false; // não-interativo não aprova destrutivo
|
|
1815
|
-
const
|
|
1816
|
-
return
|
|
1864
|
+
const d = await askDestructive(String(cmd), { en }); // resumo curto + ver completo (v)
|
|
1865
|
+
return d === true || d === 'all'; // runbook aprova passo-a-passo (sem sessão full-auto)
|
|
1817
1866
|
},
|
|
1818
1867
|
onStep: (e) => {
|
|
1819
1868
|
if (e.phase === 'start') console.log(' ' + C.indigo('◆') + ' ' + (e.desc ? C.bold(e.desc) + C.dim(' · ') : '') + C.dim(e.cmd.slice(0, 60)));
|
package/lib/agent.js
CHANGED
|
@@ -153,6 +153,15 @@ function loopDecision({ nSig, cycling, warnedThis, loopWarned, WARN, BREAK }) {
|
|
|
153
153
|
return 'ok';
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// Decisão de aprovação de passo destrutivo (PURO/testável, fonte única do mapeamento).
|
|
157
|
+
// decision vem do askApprove interativo: true = aprovou este · 'all' = aprovar TUDO na sessão
|
|
158
|
+
// (full-auto) · false = recusou. autoAll = a sessão já está em full-auto (aprova sem perguntar).
|
|
159
|
+
function decideApproval({ autoAll, decision }) {
|
|
160
|
+
if (autoAll) return { approved: true, enableAll: true };
|
|
161
|
+
if (decision === 'all') return { approved: true, enableAll: true };
|
|
162
|
+
return { approved: decision === true, enableAll: false };
|
|
163
|
+
}
|
|
164
|
+
|
|
156
165
|
// SUB-AGENTE de exploração (padrão Claude Code): contexto PRÓPRIO, só-leitura, poucos passos.
|
|
157
166
|
// A leitura pesada acontece AQUI e só o RESUMO volta pro agente principal → economia de contexto.
|
|
158
167
|
async function _subAgent({ task, k, model, cwd, lang, onStep }) {
|
|
@@ -215,10 +224,17 @@ async function _remoteApprove(comando, token, onRemote) {
|
|
|
215
224
|
/**
|
|
216
225
|
* Roda o agente. opts:
|
|
217
226
|
* token (sessão ts) · lang · yes (auto-aprova NÃO-destrutivos; destrutivo recusa)
|
|
218
|
-
*
|
|
227
|
+
* autoAll (FULL-AUTO: aprova TUDO na sessão, inclusive destrutivos — o gate anti-catástrofe
|
|
228
|
+
* de AUTO-destrutivos segue recusando na hora, full-auto não abre mão dele)
|
|
229
|
+
* onStep({name, detail, auto}) — passo iniciado (pra UI; auto=destrutivo aprovado em full-auto)
|
|
230
|
+
* onStepDone({name, ok, status, evidence}) — passo concluído (✓/✗ por etapa)
|
|
231
|
+
* askApprove(cmd|{kind,cmd}) → Promise<boolean|'all'> ('all' liga o full-auto na sessão)
|
|
219
232
|
*/
|
|
220
233
|
async function run(task, opts = {}) {
|
|
221
|
-
const { token, lang = 'pt', yes = false, model = null, confineDir = null, onStep = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
|
|
234
|
+
const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
|
|
235
|
+
// FULL-AUTO (autoridade total): aprova destrutivos sem perguntar. Começa por --yolo/--full-auto
|
|
236
|
+
// (opts.autoAll) e pode ser LIGADO no meio da sessão quando o humano responde "a" (aprovar tudo).
|
|
237
|
+
let autoApproveDestructive = !!autoAll;
|
|
222
238
|
// CWD da SESSÃO: base de todo caminho relativo e do shell. Persiste entre
|
|
223
239
|
// mensagens (o chamador passa opts.cwd e lê o out.cwd de volta). Missão usa a
|
|
224
240
|
// pasta confinada. É MUTÁVEL: a ferramenta mudar_diretorio e o "cd" no início
|
|
@@ -465,6 +481,7 @@ async function run(task, opts = {}) {
|
|
|
465
481
|
const name = (tc.function && tc.function.name) || '';
|
|
466
482
|
let input = {}; try { input = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (_) {}
|
|
467
483
|
let result;
|
|
484
|
+
let _ran = false; // true só quando uma ferramenta REALMENTE executou (não gate/bloqueio) → status ✓/✗
|
|
468
485
|
|
|
469
486
|
// ── WATCHDOG anti-loop: mede repetição ANTES de qualquer gate/execução ──
|
|
470
487
|
const _sig = loopSig(name, input);
|
|
@@ -517,9 +534,28 @@ async function run(task, opts = {}) {
|
|
|
517
534
|
// Interativo pergunta no terminal; em --yes (cron) tenta APROVAÇÃO REMOTA no Telegram do dono.
|
|
518
535
|
if (result === undefined && (name === 'executar_comando' || name === 'executar_remoto') && tools.isDestructive(input.comando)) {
|
|
519
536
|
let approved = false, remoteTried = false;
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
537
|
+
if (autoApproveDestructive) {
|
|
538
|
+
// FULL-AUTO não abre mão do anti-catástrofe: os auto-destrutivos de máquina/processo/
|
|
539
|
+
// pasta já caíram no gate acima (selfDestructiveReason). O que sobra é ~/.ts por shell
|
|
540
|
+
// (bypass dos gates dedicados skill_gerenciar/hooks) — esse NUNCA é auto-aprovado nem em
|
|
541
|
+
// --yolo: exige o humano. Todo o resto: aprova sem perguntar, mas VISÍVEL (auto:true).
|
|
542
|
+
if (core.touchesTsConfig(String(input.comando || ''))) {
|
|
543
|
+
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
544
|
+
result = { erro: lang !== 'en'
|
|
545
|
+
? 'FULL-AUTO (--yolo) NÃO auto-aprova comando que mexe em ~/.ts (skills/hooks/mcp/config do ts via shell) — isso burlaria os gates dedicados. Use a ferramenta própria (skill_gerenciar) ou rode SEM --yolo pra aprovar manualmente.'
|
|
546
|
+
: 'FULL-AUTO (--yolo) will NOT auto-approve a command that touches ~/.ts (ts skills/hooks/mcp/config via shell) — it would bypass the dedicated gates. Use the proper tool (skill_gerenciar) or run WITHOUT --yolo to approve manually.' };
|
|
547
|
+
} else {
|
|
548
|
+
approved = true;
|
|
549
|
+
onStep({ name, detail: argsShort(name, input), auto: true });
|
|
550
|
+
}
|
|
551
|
+
} else if (!yes) {
|
|
552
|
+
// Passa payload ESTRUTURADO → a UI mostra um RESUMO curto (não o heredoc inteiro).
|
|
553
|
+
const dec = await askApprove({ kind: 'destructive', cmd: String(input.comando || '') });
|
|
554
|
+
const d = decideApproval({ autoAll: false, decision: dec });
|
|
555
|
+
approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
|
|
556
|
+
} else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
|
|
557
|
+
// result === undefined evita sobrescrever a mensagem específica do bloqueio ~/.ts do full-auto
|
|
558
|
+
if (!approved && result === undefined) {
|
|
523
559
|
result = { erro: yes
|
|
524
560
|
? (remoteTried
|
|
525
561
|
? 'RECUSADO: o dono NEGOU (ou não respondeu em 2 min) a aprovação remota deste comando destrutivo. Não repita; siga sem ele e relate no resumo.'
|
|
@@ -549,7 +585,8 @@ async function run(task, opts = {}) {
|
|
|
549
585
|
const prev = input.acao === 'criar'
|
|
550
586
|
? `criar skill "${_cl(input.slug)}" — ${_cl(input.descricao || input.nome).slice(0, 120)}\n motivo: ${_cl(input.motivo).slice(0, 200)}\n instruções:\n ${_full.slice(0, CAP)}${_nota}`
|
|
551
587
|
: `melhorar skill "${_cl(input.slug)}"\n motivo: ${_cl(input.motivo).slice(0, 200)}\n trocar: ${_cl(input.buscar || (lang !== 'en' ? '(adicionar seção no fim)' : '(append section at the end)')).slice(0, 400)}\n por: ${_full.slice(0, Math.min(CAP, 400))}${_nota}`;
|
|
552
|
-
const okS = await askApprove((lang !== 'en' ? 'O agente quer ATUALIZAR as próprias skills:\n ' : 'The agent wants to UPDATE its own skills:\n ') + prev);
|
|
588
|
+
const okS = autoApproveDestructive ? true : await askApprove((lang !== 'en' ? 'O agente quer ATUALIZAR as próprias skills:\n ' : 'The agent wants to UPDATE its own skills:\n ') + prev);
|
|
589
|
+
if (okS === 'all') autoApproveDestructive = true;
|
|
553
590
|
if (!okS) {
|
|
554
591
|
result = { erro: lang !== 'en'
|
|
555
592
|
? 'O usuário RECUSOU a mudança de skill. Não repita nesta sessão; siga com a tarefa normalmente.'
|
|
@@ -566,8 +603,12 @@ async function run(task, opts = {}) {
|
|
|
566
603
|
const _js = JSON.stringify(input);
|
|
567
604
|
const _lbl = 'MCP ' + _mcp.route[name].server + ' → ' + _mcp.route[name].realName + ' ' + (_js.length > 1500 ? _js.slice(0, 1500) + ' …(+' + (_js.length - 1500) + ' chars)' : _js);
|
|
568
605
|
let approved = false, remoteTried = false;
|
|
569
|
-
if (
|
|
570
|
-
else
|
|
606
|
+
if (autoApproveDestructive) { approved = true; onStep({ name, detail: argsShort(name, input), auto: true }); }
|
|
607
|
+
else if (!yes) {
|
|
608
|
+
const dec = await askApprove(_lbl);
|
|
609
|
+
const d = decideApproval({ autoAll: false, decision: dec });
|
|
610
|
+
approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
|
|
611
|
+
} else ({ approved, remoteTried } = await _remoteApprove(_lbl, token, onRemote));
|
|
571
612
|
if (!approved) {
|
|
572
613
|
result = { erro: yes
|
|
573
614
|
? (remoteTried ? 'RECUSADO: o dono NEGOU (ou não respondeu) a aprovação remota desta ferramenta MCP mutante. Não repita; siga sem ela.' : 'RECUSADO automaticamente: ferramenta MCP mutante não roda em --yes sem aprovação remota (Telegram). Siga sem ela.')
|
|
@@ -589,8 +630,9 @@ async function run(task, opts = {}) {
|
|
|
589
630
|
// 1ª chamada a ESTE servidor na sessão pede um OK (mesmo tool de leitura): os ARGUMENTOS
|
|
590
631
|
// saem da máquina pro servidor externo, e podem ter sido influenciados por prompt-injection
|
|
591
632
|
// de algo que o agente leu. Em --yes (cron) o dono já pré-autorizou os servers → não pergunta.
|
|
592
|
-
if (!rt.needsApproval && !yes && !_mcpSeen.has(rt.server)) {
|
|
633
|
+
if (!rt.needsApproval && !yes && !autoApproveDestructive && !_mcpSeen.has(rt.server)) {
|
|
593
634
|
const okFirst = await askApprove('1ª chamada ao servidor MCP "' + rt.server + '" (tool ' + rt.realName + '). Dados sairão pra ele. Permitir este servidor nesta sessão?');
|
|
635
|
+
if (okFirst === 'all') autoApproveDestructive = true;
|
|
594
636
|
if (!okFirst) { result = { erro: 'O usuário NÃO autorizou o servidor MCP "' + rt.server + '" nesta sessão. Não use tools desse servidor; siga sem elas.' }; onStep({ name, detail: argsShort(name, input), blocked: true }); }
|
|
595
637
|
}
|
|
596
638
|
if (result === undefined) {
|
|
@@ -598,7 +640,7 @@ async function run(task, opts = {}) {
|
|
|
598
640
|
onStep({ name: 'mcp:' + rt.realName, detail: rt.server });
|
|
599
641
|
try { result = { resultado: await require('./mcp').callTool(rt.endpoint, rt.auth, rt.realName, input) }; }
|
|
600
642
|
catch (e) { result = { erro: 'MCP falhou: ' + String((e && e.message) || e).slice(0, 200) }; }
|
|
601
|
-
steps++;
|
|
643
|
+
steps++; _ran = true;
|
|
602
644
|
}
|
|
603
645
|
}
|
|
604
646
|
// LIMITE DE PESQUISA (convergência): conta buscar_web + navegador abrir/ler. Ao passar o
|
|
@@ -636,7 +678,7 @@ async function run(task, opts = {}) {
|
|
|
636
678
|
}
|
|
637
679
|
else result = { erro: 'acao inválida — use abrir|clicar|digitar|ler|print.' };
|
|
638
680
|
} catch (e) { result = { erro: String((e && e.message) || e).slice(0, 300) }; }
|
|
639
|
-
steps++;
|
|
681
|
+
steps++; _ran = true;
|
|
640
682
|
}
|
|
641
683
|
// SUB-AGENTE: 'explorar' roda em contexto próprio (só-leitura) e devolve só o resumo.
|
|
642
684
|
if (result === undefined && name === 'explorar') {
|
|
@@ -644,12 +686,12 @@ async function run(task, opts = {}) {
|
|
|
644
686
|
const sub = await _subAgent({ task: input.tarefa || input.pergunta || '', k, model, cwd, lang, onStep });
|
|
645
687
|
acc.inTok += sub._tin; acc.outTok += sub._tout; acc.cachedTok += sub._tcach || 0;
|
|
646
688
|
result = { resumo: sub.resumo };
|
|
647
|
-
steps++;
|
|
689
|
+
steps++; _ran = true;
|
|
648
690
|
}
|
|
649
691
|
if (result === undefined) {
|
|
650
692
|
onStep({ name, detail: argsShort(name, input) });
|
|
651
693
|
result = await tools.execute(name, input, { confineDir, baseDir: cwd, token });
|
|
652
|
-
steps++;
|
|
694
|
+
steps++; _ran = true;
|
|
653
695
|
// TOOLRESULT TIPADO (core.classifyToolResult): classe de erro DETERMINÍSTICA. A verdade
|
|
654
696
|
// sobre "deu certo?" vem daqui, não da narrativa do modelo. 'blocked' = política (gate),
|
|
655
697
|
// não falha de execução — não conta pro ledger nem pro sinal de erro da run.
|
|
@@ -681,6 +723,10 @@ async function run(task, opts = {}) {
|
|
|
681
723
|
&& _researchCalls >= Math.ceil(RESEARCH_MAX * 2 / 3) && _researchCalls <= RESEARCH_MAX) {
|
|
682
724
|
result = Object.assign({}, result, { _aviso: 'Você já pesquisou ' + _researchCalls + 'x (limite ' + RESEARCH_MAX + '). Se já tem o suficiente, PARE de pesquisar e ENTREGUE o resultado agora.' });
|
|
683
725
|
}
|
|
726
|
+
// STATUS POR ETAPA (UX): ✓ verde / ✗ vermelho por passo, a partir do ToolResult TIPADO
|
|
727
|
+
// (core.classifyToolResult) — o usuário vê na hora o que deu certo/errado (antes as ⚙
|
|
728
|
+
// não indicavam resultado nenhum). Só para passos que REALMENTE rodaram (não gate/bloqueio).
|
|
729
|
+
if (_ran) { const _c = core.classifyToolResult(result); onStepDone({ name, ok: _c.ok, status: _c.status, evidence: _c.evidence }); }
|
|
684
730
|
// BLOQUEIO HUMANO: o agente pediu uma ação externa que só o usuário faz →
|
|
685
731
|
// encerra o turno devolvendo o pedido; a missão pausa e chama o usuário.
|
|
686
732
|
if (result && result._needHuman) {
|
|
@@ -731,4 +777,4 @@ async function run(task, opts = {}) {
|
|
|
731
777
|
return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr };
|
|
732
778
|
}
|
|
733
779
|
|
|
734
|
-
module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision } };
|
|
780
|
+
module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval } };
|
package/lib/core.js
CHANGED
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
// — só funções puras e constantes, pra ser importável por qualquer superfície e testável.
|
|
13
13
|
|
|
14
14
|
// ── 1) GATE DE DESTRUTIVO ─────────────────────────────────────────────────────
|
|
15
|
+
// ~/.ts = config do agente (skills viram INSTRUÇÕES futuras; hooks executam comandos;
|
|
16
|
+
// mcp.json guarda auth). Tocar nisso por shell burlaria os gates dedicados
|
|
17
|
+
// (skill_gerenciar/lembrar) → prompt-injection persistente. Exige a barra antes de ".ts/"
|
|
18
|
+
// — não casa arquivos TypeScript (utils.ts). É o subconjunto "endurecido" do gate destrutivo:
|
|
19
|
+
// o ÚNICO destrutivo que o FULL-AUTO (--yolo) NÃO libera sozinho — sempre exige o humano.
|
|
20
|
+
const TS_CONFIG_RE = /[\\\/]\.ts[\\\/](skills|skills-pending|hooks|mcp|config)/i;
|
|
21
|
+
function touchesTsConfig(cmd) { return TS_CONFIG_RE.test(String(cmd || '')); }
|
|
22
|
+
|
|
15
23
|
// Padrões de comando destrutivo/irreversível — SEMPRE pedem aprovação humana.
|
|
16
24
|
// (fonte única: antes vivia em tools.js; App e Web tinham cópias divergentes.)
|
|
17
25
|
const DESTRUCTIVE = [
|
|
@@ -25,11 +33,7 @@ const DESTRUCTIVE = [
|
|
|
25
33
|
/\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
34
|
/\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
35
|
/\bsystemctl\s+(stop|disable|mask)\b/i, /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm)\b/i,
|
|
28
|
-
// ~/.ts
|
|
29
|
-
// mcp.json guarda auth). Tocar nisso por shell burlaria os gates dedicados
|
|
30
|
-
// (skill_gerenciar/lembrar) → prompt-injection persistente. Sempre pede aprovação.
|
|
31
|
-
// Exige a barra antes de ".ts/" — não casa arquivos TypeScript (utils.ts).
|
|
32
|
-
/[\\\/]\.ts[\\\/](skills|skills-pending|hooks|mcp|config)/i,
|
|
36
|
+
TS_CONFIG_RE, // ~/.ts por shell = bypass dos gates dedicados (ver comentário acima)
|
|
33
37
|
];
|
|
34
38
|
function isDestructive(cmd) { return DESTRUCTIVE.some(re => re.test(String(cmd || ''))); }
|
|
35
39
|
|
|
@@ -109,7 +113,20 @@ function classifyError(msg) {
|
|
|
109
113
|
// NÃO muta o raw. Convenções do TS: {erro} = falha; {codigo|exitCode}≠0 = falha de shell;
|
|
110
114
|
// {written|created|resumo|stdout|...} = evidência de sucesso.
|
|
111
115
|
function classifyToolResult(raw) {
|
|
112
|
-
if (raw == null
|
|
116
|
+
if (raw == null) return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: '' };
|
|
117
|
+
// Alguns adaptadores legados retornam falhas como texto. Tratar todo texto como sucesso
|
|
118
|
+
// permitia que "ERRO ao executar..." virasse etapa concluída. Mantém strings normais como
|
|
119
|
+
// sucesso, mas reconhece prefixos inequívocos de falha/bloqueio.
|
|
120
|
+
if (typeof raw !== 'object') {
|
|
121
|
+
const text = String(raw);
|
|
122
|
+
const failedText = /^\s*(?:(?:erro|error|falha|failed|bloquead[oa])(?:\s*[:\-]|\s+(?:ao|a|de|do|da|em|no|na|to|while|executing|calling|opening|reading|writing|request|operation)\b|\s*$)|operação cancelada\b|o usuário recusou\b)/i.test(text);
|
|
123
|
+
if (failedText) {
|
|
124
|
+
const errorClass = classifyError(text);
|
|
125
|
+
return { ok: false, status: errorClass === 'blocked' ? 'blocked' : 'failed', errorClass,
|
|
126
|
+
retryable: RETRYABLE_CLASSES.has(errorClass), evidence: text.replace(/\s+/g, ' ').slice(0, 300) };
|
|
127
|
+
}
|
|
128
|
+
return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: text.slice(0, 200) };
|
|
129
|
+
}
|
|
113
130
|
const exit = raw.codigo != null ? raw.codigo : (raw.exitCode != null ? raw.exitCode : null);
|
|
114
131
|
const errMsg = raw.erro || raw.error || (exit != null && exit !== 0 ? (raw.stderr || raw.stdout || ('exit ' + exit)) : '');
|
|
115
132
|
const failed = !!raw.erro || !!raw.error || (exit != null && exit !== 0);
|
|
@@ -128,7 +145,7 @@ function classifyToolResult(raw) {
|
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
module.exports = {
|
|
131
|
-
DESTRUCTIVE, isDestructive,
|
|
148
|
+
DESTRUCTIVE, isDestructive, touchesTsConfig,
|
|
132
149
|
MODEL_WINDOWS, CTX_WINDOW_DEFAULT, DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok,
|
|
133
150
|
AgentEvents, EVENT_TYPES,
|
|
134
151
|
ERROR_CLASSES, RETRYABLE_CLASSES, classifyError, classifyToolResult,
|
package/lib/i18n.js
CHANGED
|
@@ -41,6 +41,7 @@ const STR = {
|
|
|
41
41
|
['ts sentinela add "nome" --cmd "..."', 'VIGIA determinístico (regra do if, ZERO token); --escalar diagnostica se falhar; --autofix conserta (com aprovação no Telegram)'],
|
|
42
42
|
['ts sentinela instalar', 'agenda os checks no SO (cron/Task Scheduler) e avisa no Telegram'],
|
|
43
43
|
['ts agente --continuar "..."', 'retoma o trabalho anterior desta pasta'],
|
|
44
|
+
['ts agente "..." --yolo', 'FULL-AUTO: aprova TUDO na sessão, inclusive destrutivos (o gate anti-catástrofe ~/.ts/máquina segue ativo)'],
|
|
44
45
|
['ts agente "..." --navegador', 'EXPERIMENTAL: dá um Chrome real ao agente (abrir/ler/clicar/print+visão)'],
|
|
45
46
|
['ts meta "objetivo grande"', 'MISSÃO: checklist + rodadas até terminar (noturno)'],
|
|
46
47
|
['ts meta "..." --criterios prova.json', 'só declara "verificada" se os critérios executáveis (verify) passarem de verdade'],
|
|
@@ -197,6 +198,7 @@ const STR = {
|
|
|
197
198
|
['ts agente "task"', 'actually does it here: commands, files, diagnosis'],
|
|
198
199
|
['ts agente "..." --yes', 'autonomous (destructive asks on Telegram)'],
|
|
199
200
|
['ts agente --continuar "..."', 'resume this folder\'s previous work'],
|
|
201
|
+
['ts agente "..." --yolo', 'FULL-AUTO: approves EVERYTHING this session, destructive included (~/.ts & machine anti-catastrophe still on)'],
|
|
200
202
|
['ts agente "..." --navegador', 'EXPERIMENTAL: gives the agent a real Chrome (open/read/click/print+vision)'],
|
|
201
203
|
['ts sentinela add "name" --cmd "..."', 'DETERMINISTIC watch (if-rule, ZERO tokens); escalates to AI only on failure'],
|
|
202
204
|
['ts sentinela instalar', 'schedule checks on the OS (cron/Task Scheduler), alert on Telegram'],
|
package/lib/meta.js
CHANGED
|
@@ -1011,7 +1011,8 @@ async function makeChecklist(goal, token) {
|
|
|
1011
1011
|
// CRITÉRIOS EXECUTÁVEIS (verify): a prova FINAL da missão. Rodados quando os gates de
|
|
1012
1012
|
// build/run passam, ANTES de declarar 'verified'. Se algum critério do usuário falha, a
|
|
1013
1013
|
// missão NÃO é verificada (honestidade — não basta compilar/abrir; tem que CUMPRIR o pedido).
|
|
1014
|
-
// Sem critérios
|
|
1014
|
+
// Sem critérios explícitos, os gates de build/run continuam sendo a prova. Erro interno do
|
|
1015
|
+
// verificador é fail-closed: nunca transforma ausência de prova em `verified`.
|
|
1015
1016
|
async function _checkCriteria(st, dir) {
|
|
1016
1017
|
if (!st.criteria || !st.criteria.length) return true;
|
|
1017
1018
|
try {
|
|
@@ -1019,11 +1020,15 @@ async function _checkCriteria(st, dir) {
|
|
|
1019
1020
|
st.verifyReport = { passed: rep.passed, total: rep.total, allOk: rep.allOk, results: rep.results.map(r => ({ ok: r.ok, label: r.label, detail: r.detail })) };
|
|
1020
1021
|
if (!rep.allOk) st.runNote = (st.runNote || '') + ` critérios: ${rep.passed}/${rep.total} passaram · falharam: ${rep.results.filter(r => !r.ok).map(r => r.label).slice(0, 3).join(' | ')}`;
|
|
1021
1022
|
return rep.allOk;
|
|
1022
|
-
} catch (
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
st.verifyReport = { passed: 0, total: st.criteria.length, allOk: false, results: [], error: String((e && e.message) || e).slice(0, 300) };
|
|
1025
|
+
st.runNote = (st.runNote || '') + ' verificador falhou internamente; missão não marcada como verified';
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1023
1028
|
}
|
|
1024
1029
|
|
|
1025
1030
|
async function run(goal, opts = {}) {
|
|
1026
|
-
const { token, lang = 'pt', yes = false, budget = 400, maxRounds = 20, dir = process.cwd(), model = null, thinker = null, maxMinutes = 0, designer = null, design = 'auto', runGate = true, eye = null, visualLadder = true, prove = false } = opts;
|
|
1031
|
+
const { token, lang = 'pt', yes = false, autoAll = false, budget = 400, maxRounds = 20, dir = process.cwd(), model = null, thinker = null, maxMinutes = 0, designer = null, design = 'auto', runGate = true, eye = null, visualLadder = true, prove = false } = opts;
|
|
1027
1032
|
const onChecklist = opts.onChecklist || (() => {});
|
|
1028
1033
|
const onRound = opts.onRound || (() => {});
|
|
1029
1034
|
const onRoundDone = opts.onRoundDone || (() => {});
|
|
@@ -1064,12 +1069,14 @@ async function run(goal, opts = {}) {
|
|
|
1064
1069
|
}
|
|
1065
1070
|
const mk = await makeChecklist(goal + (designBrief ? '\n\n[Há um DESIGN SYSTEM definido — o checklist deve refletir a aplicação desse visual]' : '') + (archBrief ? '\n\n[Há um CONTRATO DE ARQUITETURA definido — os itens devem seguir os componentes/arquivos dele]' : ''), token);
|
|
1066
1071
|
// CRITÉRIOS (TaskSpec): usuário (--criterios) manda; senão, com --provar, combina o que o
|
|
1067
|
-
// PLANNER propôs (mk.criteria, já validado) + os TESTES da stack — tudo recompilado.
|
|
1068
|
-
// --provar,
|
|
1072
|
+
// PLANNER propôs (mk.criteria, já validado) + os TESTES da stack — tudo recompilado.
|
|
1073
|
+
// Mesmo sem --provar, critérios determinísticos derivados da stack rodam por padrão;
|
|
1074
|
+
// critérios inventados pelo planner ficam apenas como referência até o usuário pedir prova ampla.
|
|
1069
1075
|
const _V = require('./verify');
|
|
1070
1076
|
let _criteria = [], _planned = mk.criteria || [];
|
|
1071
1077
|
if (Array.isArray(opts.criteria) && opts.criteria.length) _criteria = opts.criteria;
|
|
1072
1078
|
else if (prove) _criteria = _V.compileCriteria([..._V.deriveFromStack(dir), ..._planned]).criteria.slice(0, 8);
|
|
1079
|
+
else _criteria = _V.compileCriteria(_V.deriveFromStack(dir)).criteria.slice(0, 8);
|
|
1073
1080
|
st = { goal: String(goal).slice(0, 4000), mockup: opts.mockup || null, archBrief, feasBrief, checklist: mk.itens, rounds: [], creditsSpent: (mk.credits || 0) + designCred + archCred + feasCred,
|
|
1074
1081
|
budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, criteria: _criteria, plannedCriteria: _planned, created_at: new Date().toISOString() };
|
|
1075
1082
|
save(st, dir);
|
|
@@ -1313,7 +1320,7 @@ async function run(goal, opts = {}) {
|
|
|
1313
1320
|
}
|
|
1314
1321
|
let out = null, connErr = null;
|
|
1315
1322
|
for (let att = 0; att < 3 && !out; att++) {
|
|
1316
|
-
try { out = await agent.run(task, { token, lang, yes, model: roundModel, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking }); }
|
|
1323
|
+
try { out = await agent.run(task, { token, lang, yes, autoAll, model: roundModel, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, onStepDone: opts.onStepDone, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking }); }
|
|
1317
1324
|
catch (e) {
|
|
1318
1325
|
connErr = e;
|
|
1319
1326
|
// teto de IA estourado → PAUSA limpa e resumível com CTA de upgrade (nunca segue em silêncio)
|
|
@@ -1346,14 +1353,16 @@ async function run(goal, opts = {}) {
|
|
|
1346
1353
|
// (Bug real do teste MultiApps: flash-lite bloqueou itens cujos arquivos existiam no disco.)
|
|
1347
1354
|
let marks = [];
|
|
1348
1355
|
const written = (out.actions || []).filter(a => a.name === 'escrever_arquivo' || a.name === 'editar_arquivo').map(a => String(a.target || ''));
|
|
1349
|
-
const ranOk = (out.actions || []).some(a => a.name === 'executar_comando');
|
|
1350
1356
|
const _basename = (p) => String(p).replace(/\\/g, '/').split('/').pop().toLowerCase();
|
|
1351
1357
|
const _fileHints = (txt) => (String(txt).match(/[\w.\-]+\.(kt|java|xml|gradle|json|md|txt|kts|properties|pro|png|webp|py|js|ts|html|css|sh)\b/gi) || []).map(s => s.toLowerCase());
|
|
1352
1358
|
const itemFiles = _fileHints(item.desc);
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1359
|
+
// Uma escrita/comando genérico não prova um item sem vínculo. Só há marcação automática
|
|
1360
|
+
// quando o próprio texto do item identifica o artefato e a rodada gravou esse artefato.
|
|
1361
|
+
// Itens abstratos continuam podendo ser avaliados pelo marcador, mas não "vazam" sucesso
|
|
1362
|
+
// para outros itens só porque algum arquivo/comando apareceu na mesma rodada.
|
|
1363
|
+
const wroteForItem = itemFiles.length > 0
|
|
1364
|
+
&& itemFiles.some(f => written.some(w => _basename(w) === f));
|
|
1365
|
+
if (wroteForItem) marks.push(item.id);
|
|
1357
1366
|
|
|
1358
1367
|
// Item de correção de build: NÃO usa marcador — o PORTÃO DE BUILD é a única
|
|
1359
1368
|
// autoridade (recompila de verdade). Marca provisório pra reabrir o gate; se o
|
|
@@ -1368,15 +1377,14 @@ async function run(goal, opts = {}) {
|
|
|
1368
1377
|
continue;
|
|
1369
1378
|
}
|
|
1370
1379
|
|
|
1371
|
-
// Marcador IA como REFORÇO
|
|
1372
|
-
//
|
|
1380
|
+
// Marcador IA como REFORÇO do ITEM ALVO. Não aceita mais `tambem_concluidos`: uma
|
|
1381
|
+
// narrativa de uma rodada não é evidência suficiente para concluir itens não executados.
|
|
1373
1382
|
try {
|
|
1374
1383
|
const evid = written.length ? '\n\nARQUIVOS REALMENTE GRAVADOS nesta rodada (evidência objetiva):\n' + written.map(_basename).join(', ') : '';
|
|
1375
1384
|
const mk = await _llmJson({ token, system: MARK_SYS,
|
|
1376
1385
|
user: `OBJETIVO:\n${st.goal.slice(0, 600)}\n\nCHECKLIST (contexto):\n${fmtChecklist(st.checklist)}\n\nITEM ALVO: (${item.id}) ${item.desc}\n\nRESULTADO DA RODADA:\n${String(out.text || '').slice(0, 2000)}${evid}` });
|
|
1377
1386
|
st.creditsSpent += mk.credits || 0;
|
|
1378
1387
|
if (mk.json && mk.json.passou === true) marks.push(item.id);
|
|
1379
|
-
if (Array.isArray(mk.json?.tambem_concluidos)) marks.push(...mk.json.tambem_concluidos.map(String));
|
|
1380
1388
|
} catch (_) {}
|
|
1381
1389
|
marks = [...new Set(marks)];
|
|
1382
1390
|
for (const it of st.checklist) if (marks.includes(it.id)) it.passes = true;
|
package/lib/ui.js
CHANGED
|
@@ -99,8 +99,20 @@ function ask(question) {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Resumo de 1 linha de um comando pro prompt de aprovação: a 1ª linha SIGNIFICATIVA (que
|
|
103
|
+
// num heredoc é o comando externo, ex "mysql -u root pw <<'SQL'") + quantas linhas ficam
|
|
104
|
+
// escondidas. PURO/testável — a cor e as teclas do prompt ficam no bin. Evita despejar 50+
|
|
105
|
+
// linhas de um heredoc SQL antes do usuário decidir.
|
|
106
|
+
function cmdSummary(cmd, max = 72) {
|
|
107
|
+
const raw = String(cmd == null ? '' : cmd);
|
|
108
|
+
const lines = raw.split('\n');
|
|
109
|
+
const first = (lines.find(l => l.trim()) || raw).trim();
|
|
110
|
+
const shown = first.length > max ? first.slice(0, max - 1) + '…' : first;
|
|
111
|
+
return { shown, hidden: Math.max(0, lines.length - 1), lines: lines.length };
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
const okLine = (s) => ' ' + C.ok('✔') + ' ' + s;
|
|
103
115
|
const errLine = (s) => ' ' + C.err('✖') + ' ' + s;
|
|
104
116
|
const infoLine = (s) => ' ' + C.cyan('⌁') + ' ' + s;
|
|
105
117
|
|
|
106
|
-
module.exports = { C, TTY, gradient, banner, box, spinner, md, bar, ask, okLine, errLine, infoLine };
|
|
118
|
+
module.exports = { C, TTY, gradient, banner, box, spinner, md, bar, ask, cmdSummary, okLine, errLine, infoLine };
|