terminal-smart-cli 0.97.29 → 0.97.31

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 CHANGED
@@ -1079,13 +1079,28 @@ async function pollRun(token, runId, sp) {
1079
1079
  }
1080
1080
  }
1081
1081
 
1082
+ // A run é o resumo; o motivo técnico da falha fica na etapa. Centralizar a
1083
+ // leitura evita que o usuário receba apenas "falhou" e tenha de investigar
1084
+ // logs ou a interface web para saber o que fazer em seguida.
1085
+ function runFailureMessage(run) {
1086
+ const failed = (run && Array.isArray(run.steps) ? run.steps : []).find(s => s && s.status === 'failed');
1087
+ const raw = String((failed && (failed.error_message || failed.result_summary)) || (run && run.error) || '').trim();
1088
+ const code = String((failed && failed.error_code) || '').trim();
1089
+ if (code === 'execution_unavailable' || /n[aã]o executo comandos diretamente|n[aã]o tenho acesso.*(windows|computador|m[aá]quina|sistema)|execute.*powershell/i.test(raw)) {
1090
+ return 'Esta orquestração roda na nuvem e não acessa arquivos do seu computador. Para criar, editar ou inspecionar algo local, use: ts agente "sua tarefa".';
1091
+ }
1092
+ return raw || 'Nenhuma ação foi concluída. Consulte as etapas da execução para ver o motivo.';
1093
+ }
1094
+
1082
1095
  // Fluxo completo de orquestração (usado pelo `ts run` e pelo /run do modo conversa).
1083
1096
  // askFn permite aprovar com o readline do REPL sem conflito de stdin; nunca chama process.exit.
1084
1097
  async function runFlow(goal, { askFn = ui.ask, json = false, yes = false } = {}) {
1085
1098
  const token = needToken();
1086
1099
  const sp0 = ui.spinner(runLbl('planning')).start();
1087
1100
  let r;
1088
- try { r = await api('/api/ia/orchestrate', { method: 'POST', token, body: { goal }, timeoutMs: 120000 }); }
1101
+ // `ts run` é a execução autônoma do CLI: começa no modo Confiável para ter
1102
+ // planejamento e uma recuperação objetiva. O chat simples continua Rápido.
1103
+ try { r = await api('/api/ia/orchestrate', { method: 'POST', token, body: { goal, mode: 'reliable' }, timeoutMs: 120000 }); }
1089
1104
  catch (e) { sp0.stop(); throw e; }
1090
1105
  sp0.stop();
1091
1106
  let run = r.run;
@@ -1110,7 +1125,7 @@ async function runFlow(goal, { askFn = ui.ask, json = false, yes = false } = {})
1110
1125
  const result = run.final_result || run.result || '';
1111
1126
  if (result) { console.log(''); console.log(ui.md(String(result)).split('\n').map(l => ' ' + l).join('\n')); console.log(''); }
1112
1127
  } else if (run.status === 'failed') {
1113
- console.error(ui.errLine(C.bold(T.run_failed) + (run.error ? C.dim(' · ' + String(run.error).slice(0, 140)) : '')));
1128
+ console.error(ui.errLine(C.bold(T.run_failed) + C.dim(' · ' + runFailureMessage(run).slice(0, 320))));
1114
1129
  } else {
1115
1130
  console.log(ui.infoLine(T.run_paused(runLbl(run.status))));
1116
1131
  }
@@ -1433,7 +1448,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1433
1448
  // HEADLESS stream-json: fecha com assistant (texto) + result (métricas) e sai.
1434
1449
  if (streamJson) {
1435
1450
  _emit(core.AgentEvents.assistant(out.text || ''));
1436
- _emit(core.AgentEvents.result({ steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context || null, needHuman: out.needHuman || null, toolErrors: out.toolErrors || [], guard: out.guard || null, missionCache: out.missionCache || 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 }));
1451
+ _emit(core.AgentEvents.result({ steps: out.steps, credits: out.credits, tokens: out.tokens, context: out.context || null, needHuman: out.needHuman || null, guard: out.guard || null, observability: out.observability || null, duration_ms: Date.now() - t0, worktree: _wt ? { branch: _wt.branch, changed: (_wtInfo && _wtInfo.changed) || 0 } : null }));
1437
1452
  return;
1438
1453
  }
1439
1454
  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, guard: out.guard || null })); return; }
@@ -1531,7 +1546,7 @@ async function acpCmd() {
1531
1546
  // token pode faltar — o servidor responde o handshake mesmo assim e só recusa no prompt
1532
1547
  // (mensagem "rode ts login"). NÃO escreve NADA em stdout aqui (corromperia o JSON-RPC).
1533
1548
  const { acpServer } = require('../lib/acp');
1534
- acpServer({ token: cfg.token || null, lang: cfg.lang || 'pt' });
1549
+ acpServer({ token: cfg.token || null, lang: cfg.lang || 'pt', plan: process.env.TS_ACCOUNT_PLAN || cfg.plan || 'free' });
1535
1550
  // fica vivo lendo stdin até o editor fechar (rl 'close' → process.exit).
1536
1551
  return new Promise(() => {});
1537
1552
  }
@@ -1546,6 +1561,16 @@ async function evalCmd(words) {
1546
1561
  const _val = (names) => { const i = process.argv.findIndex(a => names.includes(a)); return i >= 0 ? (process.argv[i + 1] || null) : null; };
1547
1562
  const model = _val(['--modelo', '--model']); // executor do agente (bake-off)
1548
1563
  const judgeModel = _val(['--juiz', '--judge']) || undefined; // modelo do juiz (default barato)
1564
+ // Evals precisam de orçamento próprio: o laboratório não pode ficar preso
1565
+ // no teto padrão de uma missão só porque um provedor demorou a encerrar.
1566
+ const _evalSteps = Number(_val(['--passos', '--steps']));
1567
+ const _evalSeconds = Number(_val(['--tempo-max-seg', '--max-seconds']));
1568
+ const _evalCredits = Number(_val(['--orcamento-creditos', '--max-creditos']));
1569
+ const evalLimits = {
1570
+ maxIter: Number.isFinite(_evalSteps) && _evalSteps > 0 ? Math.min(60, Math.floor(_evalSteps)) : undefined,
1571
+ maxDurationMs: Number.isFinite(_evalSeconds) && _evalSeconds > 0 ? Math.min(3600, _evalSeconds) * 1000 : undefined,
1572
+ maxCredits: Number.isFinite(_evalCredits) && _evalCredits > 0 ? Math.floor(_evalCredits) : undefined,
1573
+ };
1549
1574
  const smoke = FLAGS.has('--smoke');
1550
1575
  // caminho da suíte = positional que não seja valor de --modelo/--juiz. Prefere um que
1551
1576
  // termine em .json ou exista em disco (robusto quando o valor de --modelo bate com o nome).
@@ -1585,7 +1610,7 @@ async function evalCmd(words) {
1585
1610
  let mx;
1586
1611
  try {
1587
1612
  mx = await evalMod.runMatrix(suite, {
1588
- token, lang: cfg.lang || 'pt', judgeModel, models,
1613
+ token, lang: cfg.lang || 'pt', judgeModel, models, ...evalLimits,
1589
1614
  onModel: ({ model, phase, row }) => { if (JSON_OUT) return; if (phase === 'start') spM.text((en ? 'testing ' : 'testando ') + model + '…'); else if (phase === 'done') { spM.stop(); console.log(' ' + C.cyan('◆') + ' ' + C.bold(model) + C.dim(' ' + row.passed + '/' + row.total + ' · score ' + row.avgScore + ' · ' + row.credits + ' cr') + (row.error ? C.err(' ' + row.error) : '')); spM.start(); } },
1590
1615
  onCase: ({ phase, id, pass, score }) => { if (!JSON_OUT && phase === 'done') { spM.stop(); console.log(' ' + (pass ? C.ok('✔') : C.err('✗')) + C.dim(' ' + id + ' ' + score)); spM.start(); } },
1591
1616
  });
@@ -1611,7 +1636,7 @@ async function evalCmd(words) {
1611
1636
  let res;
1612
1637
  try {
1613
1638
  res = await evalMod.runSuite(suite, {
1614
- token, lang: cfg.lang || 'pt', model, judgeModel,
1639
+ token, lang: cfg.lang || 'pt', model, judgeModel, ...evalLimits,
1615
1640
  onCase: ({ i, total, id, phase, pass, score }) => {
1616
1641
  if (JSON_OUT) return;
1617
1642
  if (phase === 'done') {
@@ -1733,6 +1758,47 @@ async function vpsCmd(words) {
1733
1758
  } catch (e) { sp && sp.stop && sp.stop(); console.log('\n ' + C.err('✗') + ' ' + e.message + '\n'); }
1734
1759
  }
1735
1760
 
1761
+ // ── ts gateway: aliases OpenSSH com operação forçada ────────────────────────
1762
+ function gatewayCmd(words) {
1763
+ const gateways = require('../lib/gateways');
1764
+ const sub = String(words[0] || 'listar').toLowerCase();
1765
+ const flag = (names) => { const i = rawArgs.findIndex(a => names.includes(a)); return i >= 0 ? rawArgs[i + 1] : null; };
1766
+ // PowerShell separa valores com vírgula em múltiplos argumentos quando eles
1767
+ // não estão entre aspas. Aceitamos as duas formas: status,logs e "status,logs".
1768
+ const flagList = (names) => {
1769
+ const i = rawArgs.findIndex(a => names.includes(a));
1770
+ if (i < 0) return null;
1771
+ const values = [];
1772
+ for (let p = i + 1; p < rawArgs.length && !String(rawArgs[p]).startsWith('-'); p++) values.push(rawArgs[p]);
1773
+ return values.join(',');
1774
+ };
1775
+ if (['listar', 'list', 'ls', 'ver', 'status'].includes(sub)) {
1776
+ const rows = gateways.list();
1777
+ if (!rows.length) { console.log(ui.infoLine('Nenhum gateway configurado. Exemplo: ts gateway adicionar producao --alias meu-gateway --operacoes status,logs,deploy')); return; }
1778
+ console.log('');
1779
+ for (const item of rows) console.log(` ${C.cyan(item.name)} ${C.bold(item.label)}${C.dim(' · alias ' + item.sshAlias + ' · ' + item.operations.join(', '))}`);
1780
+ console.log('');
1781
+ return;
1782
+ }
1783
+ if (['adicionar', 'add', 'set', 'configurar', 'config'].includes(sub)) {
1784
+ const name = words[1];
1785
+ const alias = flag(['--alias', '--ssh-alias']);
1786
+ const operations = flagList(['--operacoes', '--operations']) || 'status';
1787
+ const label = flag(['--nome', '--label']) || name;
1788
+ try {
1789
+ const item = gateways.save(name, { label, sshAlias: alias, operations });
1790
+ console.log(ui.okLine(`Gateway salvo: ${item.label} · alias ${item.sshAlias} · ${item.operations.join(', ')}`));
1791
+ } catch (e) { console.log(ui.errLine(e.message)); }
1792
+ return;
1793
+ }
1794
+ if (['remover', 'remove', 'rm', 'apagar'].includes(sub)) {
1795
+ const removed = gateways.remove(words[1]);
1796
+ console.log(removed ? ui.okLine('Gateway removido.') : ui.infoLine('Gateway não encontrado.'));
1797
+ return;
1798
+ }
1799
+ console.log(ui.infoLine('Uso: ts gateway listar | adicionar <nome> --alias <alias-openssh> --operacoes status,logs,deploy | remover <nome>'));
1800
+ }
1801
+
1736
1802
  // ── ts meta: MISSÃO longa (checklist + rodadas até terminar — modo noturno) ──
1737
1803
  function _metaChecklistBox(itens) {
1738
1804
  return ui.box(itens.map(it =>
@@ -3195,6 +3261,7 @@ function recallCmd(args) {
3195
3261
  case 'memoria': case 'memória': case 'memory': return memoriaCmd(POS.slice(1));
3196
3262
  case 'recall': case 'lembrei': case 'sessoes': case 'sessões': return recallCmd(POS.slice(1));
3197
3263
  case 'vps': case 'servidor': return vpsCmd(POS.slice(1));
3264
+ case 'gateway': case 'gateways': return gatewayCmd(POS.slice(1));
3198
3265
  case 'site': case 'sites': case 'pagina': return siteCmd(POS.slice(1));
3199
3266
  case 'cloud': case 'nuvem': return cloudCmd(POS.slice(1));
3200
3267
  case 'diagnosticar': case 'diagnose': case 'investigar': case 'debug': return diagnosticarCmd(POS.slice(1));
package/lib/acp.js CHANGED
@@ -33,7 +33,7 @@ function _kindOf(name) {
33
33
  return 'other';
34
34
  }
35
35
 
36
- function acpServer({ token, lang = 'pt' }) {
36
+ function acpServer({ token, lang = 'pt', plan = 'free' }) {
37
37
  const write = (obj) => { try { process.stdout.write(JSON.stringify(obj) + '\n'); } catch (_) {} };
38
38
  const log = (...a) => { try { process.stderr.write('[ts acp] ' + a.join(' ') + '\n'); } catch (_) {} };
39
39
  const notify = (method, params) => write({ jsonrpc: '2.0', method, params });
@@ -96,25 +96,42 @@ function acpServer({ token, lang = 'pt' }) {
96
96
  sess.cancelled = false;
97
97
  sess.running = true;
98
98
  let out = null;
99
+ const openSteps = new Map();
100
+ const beginStep = ({ name, detail, blocked, phase, model, retry }) => {
101
+ const tcid = 'tc-' + (++seq);
102
+ const queue = openSteps.get(name) || [];
103
+ queue.push(tcid); openSteps.set(name, queue);
104
+ notify('session/update', { sessionId: sid, update: {
105
+ sessionUpdate: 'tool_call', toolCallId: tcid,
106
+ title: name, rawName: name, detail: detail || '', phase: phase || '', model: model || '',
107
+ kind: _kindOf(name), status: blocked ? 'failed' : 'in_progress',
108
+ } });
109
+ if (blocked || retry || ['compactar_contexto', 'gateway'].includes(String(name))) {
110
+ queue.shift(); if (queue.length) openSteps.set(name, queue); else openSteps.delete(name);
111
+ notify('session/update', { sessionId: sid, update: {
112
+ sessionUpdate: 'tool_call_update', toolCallId: tcid,
113
+ status: blocked ? 'failed' : 'completed', evidence: String(detail || '').slice(0, 800),
114
+ } });
115
+ }
116
+ return tcid;
117
+ };
118
+ const finishStep = ({ name, ok, status, evidence }) => {
119
+ const queue = openSteps.get(name) || [];
120
+ const tcid = queue.shift() || beginStep({ name, detail: '' });
121
+ if (queue.length) openSteps.set(name, queue); else openSteps.delete(name);
122
+ notify('session/update', { sessionId: sid, update: {
123
+ sessionUpdate: 'tool_call_update', toolCallId: tcid,
124
+ status: ok === false || status === 'error' ? 'failed' : 'completed',
125
+ evidence: String(evidence || '').slice(0, 800),
126
+ } });
127
+ };
99
128
  try {
100
129
  out = await agent.run(text, {
101
- token, lang, yes: false, cwd: sess.cwd, confineDir: null,
130
+ token, lang, yes: false, cwd: sess.cwd, confineDir: null, accountPlan: plan,
102
131
  shouldStop: () => sess.cancelled, // cancelamento cooperativo: para no próximo passo
103
- onThinking: () => {},
104
- onStep: ({ name, detail, blocked }) => {
105
- const tcid = 'tc-' + (++seq); // id GLOBAL → nunca colide entre prompts/sessões
106
- // tool_call (início) + tool_call_update (fim) — nosso onStep dispara já
107
- // com o passo concluído, então mandamos os dois em sequência.
108
- notify('session/update', { sessionId: sid, update: {
109
- sessionUpdate: 'tool_call', toolCallId: tcid,
110
- title: name + (detail ? ' · ' + detail : ''), kind: _kindOf(name),
111
- status: blocked ? 'failed' : 'in_progress',
112
- } });
113
- notify('session/update', { sessionId: sid, update: {
114
- sessionUpdate: 'tool_call_update', toolCallId: tcid,
115
- status: blocked ? 'failed' : 'completed',
116
- } });
117
- },
132
+ onThinking: (progress) => notify('session/update', { sessionId: sid, update: Object.assign({ sessionUpdate: 'agent_progress' }, progress || {}) }),
133
+ onStep: beginStep,
134
+ onStepDone: finishStep,
118
135
  askApprove: async (cmd) => {
119
136
  if (sess.cancelled) return false;
120
137
  // pede a decisão ao EDITOR (o usuário aprova/recusa na UI do IDE)
@@ -145,7 +162,12 @@ function acpServer({ token, lang = 'pt' }) {
145
162
  if (sess.cancelled) return { stopReason: 'cancelled' };
146
163
  // texto final do agente → chunk de mensagem
147
164
  if (out && out.text) notify('session/update', { sessionId: sid, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: out.text } } });
148
- return { stopReason: 'end_turn' };
165
+ return {
166
+ stopReason: 'end_turn',
167
+ usage: out ? { inTok: out.tokens && out.tokens.inTok || 0, outTok: out.tokens && out.tokens.outTok || 0, cacheTok: out.tokens && out.tokens.cachedTok || 0, credits: out.credits || 0, model: out.model || '' } : {},
168
+ report: out && out.report || null,
169
+ completion: out && out.completion || null,
170
+ };
149
171
  },
150
172
  // NOTIFICAÇÃO (sem id): marca cancelamento (o agente para no próximo passo via
151
173
  // shouldStop) E destrava qualquer permissão em voo DESTA sessão (→ nega na hora),