terminal-smart-cli 0.97.12 → 0.97.14

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
@@ -125,6 +125,26 @@ function privacyCmd() {
125
125
  }
126
126
 
127
127
  // ── Login por código (device-code) ───────────────────────────────────────────
128
+ function diagnosticsCmd(args) {
129
+ const action = String((args && args[0]) || 'status').toLowerCase();
130
+ const en = (cfg.lang || 'pt') === 'en';
131
+ if (['on', 'ativar', 'ligar'].includes(action)) {
132
+ cfg = config.save({ diagnosticsEnabled: true });
133
+ console.log(ui.okLine(en ? 'Sanitized reliability diagnostics enabled.' : 'Diagnosticos sanitizados de confiabilidade ativados.'));
134
+ return;
135
+ }
136
+ if (['off', 'desativar', 'desligar'].includes(action)) {
137
+ cfg = config.save({ diagnosticsEnabled: false });
138
+ console.log(ui.okLine(en ? 'Sanitized reliability diagnostics disabled.' : 'Diagnosticos sanitizados de confiabilidade desativados.'));
139
+ return;
140
+ }
141
+ const active = require('../lib/evolution-telemetry').enabled(cfg);
142
+ console.log(ui.infoLine(en
143
+ ? `Reliability diagnostics: ${active ? 'enabled' : 'disabled'}. Sends only sanitized error signatures, tool, stage, version and recovery status; never prompts, file contents, e-mails, paths, tokens or secrets.`
144
+ : `Diagnosticos de confiabilidade: ${active ? 'ativados' : 'desativados'}. Enviam somente assinatura sanitizada do erro, ferramenta, etapa, versao e recuperacao; nunca prompts, conteudo de arquivos, e-mails, caminhos, tokens ou segredos.`));
145
+ console.log(C.dim(en ? ' Use: ts diagnostics on|off' : ' Use: ts diagnosticos ativar|desativar'));
146
+ }
147
+
128
148
  async function accountCmd(args) {
129
149
  const action = String((args && args[0]) || '').toLowerCase();
130
150
  const accountUrl = base() + '/minha-conta';
@@ -269,6 +289,17 @@ async function imageCmd(args) {
269
289
  confirmed: true,
270
290
  },
271
291
  });
292
+ if (result && result.jobId && (result.async || result.status === 'processing')) {
293
+ const { waitForImageJob } = require('../lib/image-job');
294
+ result = await waitForImageJob(result.jobId, {
295
+ request: route => api(route, { token, timeoutMs: 30000, retry: true }),
296
+ onProgress: ({ attempt }) => {
297
+ if (attempt === 5 && sp && typeof sp.text === 'function') sp.text(en
298
+ ? 'generating Premium image — this may take a few minutes…'
299
+ : 'gerando imagem Premium — isso pode levar alguns minutos…');
300
+ },
301
+ });
302
+ }
272
303
  } finally { sp.stop(); }
273
304
  if (!result || !result.url) throw new Error(en ? 'Image provider returned no image.' : 'O provedor não retornou uma imagem.');
274
305
  const fs = require('fs'), os = require('os'), path = require('path');
@@ -293,6 +324,75 @@ async function imageCmd(args) {
293
324
  if (JSON_OUT) console.log(JSON.stringify(output));
294
325
  else console.log('\n' + ui.okLine((en ? 'image saved: ' : 'imagem salva: ') + C.cyan(target)) + '\n ' + C.dim(`${result.model} · ${result.charged} créditos · saldo ${result.remaining}`));
295
326
  }
327
+
328
+ // `ts video` continua analisando URLs. Geração usa um nome separado para não
329
+ // quebrar scripts existentes: `ts gerar-video "descrição"`.
330
+ async function generateVideoCmd(args) {
331
+ const token = needToken();
332
+ const en = cfg.lang === 'en';
333
+ const prompt = args.join(' ').trim() || await readStdin();
334
+ if (!prompt || prompt.length < 8) throw new ApiError(en ? 'Describe the video.' : 'Descreva o vídeo (mínimo de 8 caracteres).', { code: 'invalid_prompt' });
335
+ const catalog = await api('/api/ia/video/options', { token });
336
+ const options = Array.isArray(catalog.options) ? catalog.options : [];
337
+ let choice = FLAGS.has('--premium') ? 'premium'
338
+ : (FLAGS.has('--economico') || FLAGS.has('--economy')) ? 'economy'
339
+ : (FLAGS.has('--sem-video') || FLAGS.has('--none')) ? 'none' : '';
340
+ if (!choice && (!process.stdin.isTTY || JSON_OUT)) {
341
+ throw new ApiError(en ? 'Choose --premium, --economy or --none.' : 'Escolha --premium, --economico ou --sem-video. Vídeo pago nunca é aprovado silenciosamente.', { code: 'video_confirmation_required' });
342
+ }
343
+ if (!choice) {
344
+ const byId = id => options.find(option => option.id === id) || {};
345
+ const economy = byId('economy'), premium = byId('premium');
346
+ console.log('\n' + ui.box([
347
+ C.bold(en ? 'Generate video' : 'Gerar vídeo'),
348
+ C.dim(en ? 'The charge happens only after secure delivery.' : 'A cobrança só acontece depois da entrega segura.'), '',
349
+ ` ${C.cyan('1')} ${C.bold(economy.label || 'Econômico · Veo Lite')} ${C.dim((economy.estimatedCredits || '?') + ' créditos')}${economy.allowed === false ? C.warn(' · requer Pro') : ''}`,
350
+ ` ${C.cyan('2')} ${C.bold(premium.label || 'Premium · Sora 2')} ${C.dim((premium.estimatedCredits || '?') + ' créditos')}${premium.allowed === false ? C.warn(' · requer Pro') : ''}`,
351
+ ` ${C.cyan('3')} ${C.bold(en ? 'Do not generate' : 'Não gerar')} ${C.dim(en ? 'no charge' : 'sem cobrança')}`, '',
352
+ C.dim((en ? 'Available ' : 'Disponível ') + (catalog.available ?? catalog.remaining ?? 0) + ' créditos'),
353
+ ], { title: 'ts gerar-video' }));
354
+ const answer = String(await ui.ask(C.dim(en ? ' Choose 1, 2 or 3 › ' : ' Escolha 1, 2 ou 3 › '))).trim();
355
+ choice = answer === '1' ? 'economy' : answer === '2' ? 'premium' : 'none';
356
+ }
357
+ const selected = options.find(option => option.id === choice);
358
+ if (!selected || selected.allowed === false) throw new ApiError(en ? 'Video generation requires Pro.' : 'Geração de vídeo requer plano Pro.', { code: 'plan_limit' });
359
+ if (choice === 'none') { if (JSON_OUT) console.log(JSON.stringify({ ok: true, skipped: true, charged: 0 })); else console.log(ui.infoLine(en ? 'No video generated.' : 'Nenhum vídeo gerado e nenhum crédito cobrado.')); return; }
360
+ const conversationId = await ensureConv(token);
361
+ const spinner = JSON_OUT ? { stop() {}, text() {} } : ui.spinner(en ? 'starting video…' : 'iniciando vídeo…').start();
362
+ let submitted;
363
+ try {
364
+ submitted = await api('/api/ia/video', { method: 'POST', token, timeoutMs: 120000, body: { conversationId, prompt, choice, confirmed: true, aspectRatio: '16:9' } });
365
+ const id = submitted && submitted.job && submitted.job.id;
366
+ if (!id) throw new Error(en ? 'Video job was not created.' : 'O job de vídeo não foi criado.');
367
+ let job;
368
+ for (let attempt = 0; attempt < 180; attempt++) {
369
+ await new Promise(resolve => setTimeout(resolve, 5000));
370
+ const state = await api('/api/ia/video/jobs/' + encodeURIComponent(id), { token, timeoutMs: 60000 });
371
+ job = state.job || {};
372
+ spinner.text((en ? 'generating video' : 'gerando vídeo') + ` · ${job.progress || 0}%`);
373
+ if (job.status === 'failed') throw new Error(job.error || (en ? 'Video failed.' : 'Falha ao gerar vídeo.'));
374
+ if (job.status === 'completed') break;
375
+ }
376
+ if (!job || job.status !== 'completed' || !job.contentUrl) throw new Error(en ? 'Video is still processing.' : 'O vídeo continua em processamento no servidor.');
377
+ const response = await fetch(base() + job.contentUrl, { headers: { 'x-session-token': token }, signal: AbortSignal.timeout(600000) });
378
+ if (!response.ok) throw new Error('Download HTTP ' + response.status);
379
+ const fs = require('fs'), os = require('os'), path = require('path');
380
+ const bytes = Buffer.from(await response.arrayBuffer());
381
+ if (bytes.length < 1024 || bytes.length > 250 * 1024 * 1024) throw new Error(en ? 'Invalid video payload.' : 'Conteúdo de vídeo inválido.');
382
+ const target = path.join(os.homedir(), 'Downloads', `terminal-smart-video-${Date.now()}.mp4`);
383
+ const partial = target + '.partial-' + process.pid;
384
+ try {
385
+ fs.writeFileSync(partial, bytes, { flag: 'wx' });
386
+ fs.renameSync(partial, target);
387
+ } catch (error) {
388
+ try { fs.unlinkSync(partial); } catch (_) {}
389
+ throw error;
390
+ }
391
+ const output = { ok: true, path: target, model: job.model, charged: job.charged, choice, seconds: job.seconds };
392
+ spinner.stop();
393
+ if (JSON_OUT) console.log(JSON.stringify(output)); else console.log('\n' + ui.okLine((en ? 'video saved: ' : 'vídeo salvo: ') + C.cyan(target)) + '\n ' + C.dim(`${job.model} · ${job.charged} créditos · ${job.seconds}s`));
394
+ } finally { spinner.stop(); }
395
+ }
296
396
  // Envia uma mensagem na conversa ativa e devolve o texto (recria a conversa 1x se apagada na web).
297
397
  async function sendMessage(token, content) {
298
398
  const doStream = async (convId) => {
@@ -1266,7 +1366,7 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null, maxIter
1266
1366
  const _maxDurationMs = 1000 * (_ti >= 0 ? Number(process.argv[_ti + 1]) : (_inlineMaxSeconds || 0));
1267
1367
  const _maxTokens = _tki >= 0 ? Number(process.argv[_tki + 1]) : _inlineMaxTokens;
1268
1368
  out = await agent.run(task, {
1269
- token, lang: cfg.lang || 'pt', yes: YES, autoAll: (YOLO || _inlineYolo || autoAllIn), model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser, allowedTools, maxIter: _maxPassos,
1369
+ token, lang: cfg.lang || 'pt', yes: YES, autoAll: (YOLO || _inlineYolo || autoAllIn), model, priorMessages, cwd: startCwd, readOnly, plan, accountPlan: cfg.plan || 'free', browser: useBrowser, allowedTools, maxIter: _maxPassos,
1270
1370
  maxCredits: _maxCredits, maxDurationMs: _maxDurationMs || undefined, maxTokens: _maxTokens,
1271
1371
  onThinking: (p) => {
1272
1372
  if (p && typeof p === 'object') {
@@ -1734,7 +1834,7 @@ async function metaCmd() {
1734
1834
  // Com --noturno, quando a run PAUSA por falta de crédito, esperamos o intervalo
1735
1835
  // e RE-INVOCAMOS run() — que retoma sozinho do .ts-meta.json — até concluir ou
1736
1836
  // bater um teto (créditos totais / janelas / horas / travamento sem progresso).
1737
- const RETOMAVEL = new Set(['no_credits', 'budget', 'connection']); // pausas que valem re-tentar
1837
+ const RETOMAVEL = new Set(['no_credits', 'budget', 'connection', 'model_no_action', 'invalid_artifact']); // pausas que valem re-tentar
1738
1838
  const _sleep = (ms) => new Promise(r => setTimeout(r, ms));
1739
1839
  let st = null, janela = 0, prevPend = null, semProgresso = 0;
1740
1840
  const loopStart = Date.now();
@@ -1769,7 +1869,11 @@ async function metaCmd() {
1769
1869
  onRoundDone: ({ checklist, spent }) => {
1770
1870
  sp.stop();
1771
1871
  const d = checklist.filter(i => i.passes).length;
1772
- console.log(' ' + C.dim(T.meta_progress(d, checklist.length, spent, budget)) + '\n');
1872
+ // On resumed missions `budget` is only the new window size, while
1873
+ // `spent` is cumulative. Display the persisted cumulative ceiling.
1874
+ const live = metaMod.load(dir);
1875
+ const cumulativeBudget = live && Number(live.budget) > 0 ? Number(live.budget) : budget;
1876
+ console.log(' ' + C.dim(T.meta_progress(d, checklist.length, spent, cumulativeBudget)) + '\n');
1773
1877
  sp.text(T.meta_marking).start();
1774
1878
  },
1775
1879
  });
@@ -1835,6 +1939,12 @@ async function metaCmd() {
1835
1939
  console.log(' ' + C.dim('Resolva o bloqueio manualmente e rode "ts meta", ou ajuste o objetivo com --novo.'));
1836
1940
  } else if (st.pause_reason === 'timeout') {
1837
1941
  console.log(ui.infoLine('Tempo limite atingido. Retome com "ts meta".'));
1942
+ } else if (st.pause_reason === 'model_no_action') {
1943
+ console.log(ui.infoLine('O executor respondeu sem criar o artefato. A retomada usará geração direta ou outro modelo; rode "ts meta".'));
1944
+ } else if (st.pause_reason === 'connection') {
1945
+ console.log(ui.infoLine('O provedor interrompeu a geração. O progresso foi salvo; rode "ts meta" para retomar.'));
1946
+ } else if (st.pause_reason === 'invalid_artifact') {
1947
+ console.log(ui.infoLine('O artefato gerado não passou na validação. As partes válidas foram salvas; rode "ts meta".'));
1838
1948
  } else {
1839
1949
  console.log(ui.infoLine(st.pause_reason === 'budget' ? T.meta_paused_budget : T.meta_paused_rounds));
1840
1950
  metaMod.notify(token, T.meta_notify_paused(st.goal, done, st.checklist.length, st.pause_reason));
@@ -2933,11 +3043,18 @@ async function uso() {
2933
3043
 
2934
3044
  async function quem() {
2935
3045
  const token = needToken();
2936
- let ok = false;
2937
- try { const chk = await api('/api/auth/check', { token }); ok = !!(chk && (chk.success || chk.authenticated)); } catch (_) {}
3046
+ let ok = false, planoAtual = cfg.plan || '?';
3047
+ try {
3048
+ const [chk, conta] = await Promise.all([
3049
+ api('/api/auth/check', { token }),
3050
+ api('/api/credits', { token }),
3051
+ ]);
3052
+ ok = !!(chk && (chk.success || chk.authenticated));
3053
+ if (conta && conta.plan) planoAtual = conta.plan;
3054
+ } catch (_) {}
2938
3055
  console.log('\n' + ui.box([
2939
3056
  C.dim(T.quem_user + ': ') + C.bold(cfg.username || '?') + (ok ? ' ' + C.ok('•') : ' ' + C.err('• offline')),
2940
- C.dim(T.quem_plan + ': ') + (cfg.plan || '?'),
3057
+ C.dim(T.quem_plan + ': ') + planoAtual,
2941
3058
  C.dim(T.quem_server + ': ') + base(),
2942
3059
  C.dim(T.quem_conv + ': ') + (cfg.convId ? '#' + cfg.convId : '—'),
2943
3060
  ], { title: T.quem_title }) + '\n');
@@ -2958,13 +3075,22 @@ async function integracoesCmd(args) {
2958
3075
  const [google, microsoft] = await Promise.all([getStatus('google'), getStatus('microsoft')]);
2959
3076
  const data = { google, microsoft };
2960
3077
  if (JSON_OUT) { console.log(JSON.stringify(data)); return; }
2961
- const line = (label, item) => C.bold(label.padEnd(22)) + (item.connected ? C.ok(en ? 'connected' : 'conectado') + C.dim(item.email ? ` · ${item.email}` : '') : C.dim(en ? 'not connected' : 'não conectado'));
2962
- console.log('\n' + ui.box([line('Google Workspace', google), line('Microsoft/Outlook', microsoft), '', C.dim(en ? 'Connect: ts integrations connect google|microsoft' : 'Conectar: ts integracoes conectar google|microsoft'), C.dim(en ? 'Manage on web: terminalsmart.com.br/integracoes' : 'Gerenciar na Web: terminalsmart.com.br/integracoes')], { title: en ? 'Integrations' : 'Integrações' }) + '\n');
3078
+ const line = (label, item) => C.bold(label.padEnd(22)) + (item.connected ? C.ok(en ? 'connected' : 'conectado') + C.dim(item.email ? ` · ${item.email}` : '') + C.dim(item.accessLevel ? ` · ${item.accessLevel === 'advanced' ? (en ? 'advanced beta' : 'avançado beta') : (en ? 'essential' : 'essencial')}` : '') : C.dim(en ? 'not connected' : 'não conectado'));
3079
+ console.log('\n' + ui.box([line('Google Workspace', google), line('Microsoft/Outlook', microsoft), '', C.dim(en ? 'Connect: ts integrations connect google [essential|advanced]' : 'Conectar: ts integracoes conectar google [essencial|avancado]'), C.dim(en ? 'Manage on web: terminalsmart.com.br/integracoes' : 'Gerenciar na Web: terminalsmart.com.br/integracoes')], { title: en ? 'Integrations' : 'Integrações' }) + '\n');
3080
+ return;
3081
+ }
3082
+ if (['arquivos', 'files', 'drive'].includes(sub)) {
3083
+ const url = 'https://terminalsmart.com.br/integracoes#google-files';
3084
+ if (JSON_OUT) { console.log(JSON.stringify({ provider: 'google', url })); return; }
3085
+ console.log(ui.infoLine(en ? 'Opening Google Drive file selection…' : 'Abrindo a seleção de arquivos do Google Drive…'));
3086
+ try { require('child_process').exec(process.platform === 'win32' ? `start "" "${url}"` : process.platform === 'darwin' ? `open "${url}"` : `xdg-open "${url}"`); } catch (_) {}
2963
3087
  return;
2964
3088
  }
2965
3089
  if (!provider) { console.error(ui.infoLine(en ? 'Choose google or microsoft.' : 'Escolha google ou microsoft.')); process.exit(2); }
2966
3090
  if (['conectar', 'connect', 'reconectar', 'reconnect'].includes(sub)) {
2967
- const data = await api(`/api/integrations/${provider}/connect`, { method: 'POST', token, body: { source: 'cli' } });
3091
+ const requestedLevel = String(args[2] || '').toLowerCase();
3092
+ const accessLevel = provider === 'google' && ['avancado', 'advanced'].includes(requestedLevel) ? 'advanced' : 'essential';
3093
+ const data = await api(`/api/integrations/${provider}/connect`, { method: 'POST', token, body: { source: 'cli', ...(provider === 'google' ? { accessLevel } : {}) } });
2968
3094
  if (JSON_OUT) { console.log(JSON.stringify({ provider, authUrl: data.authUrl })); return; }
2969
3095
  console.log('\n' + ui.box([C.bold(en ? `Authorize ${names[provider]}` : `Autorize ${names[provider]}`), '', data.authUrl, '', C.dim(en ? 'After authorization, run: ts integrations' : 'Depois de autorizar, rode: ts integracoes')], { title: 'OAuth' }) + '\n');
2970
3096
  try { require('child_process').exec(process.platform === 'win32' ? `start "" "${data.authUrl}"` : process.platform === 'darwin' ? `open "${data.authUrl}"` : `xdg-open "${data.authUrl}"`); } catch (_) {}
@@ -2977,7 +3103,7 @@ async function integracoesCmd(args) {
2977
3103
  await api(`/api/integrations/${provider}/disconnect`, { method: 'POST', token, body: { confirmed: true } });
2978
3104
  console.log(ui.okLine(`${names[provider]} ${en ? 'disconnected' : 'desconectado'}`)); return;
2979
3105
  }
2980
- console.log(ui.infoLine(en ? 'Usage: ts integrations [status|connect|disconnect] [google|microsoft]' : 'Uso: ts integracoes [status|conectar|desconectar] [google|microsoft]'));
3106
+ console.log(ui.infoLine(en ? 'Usage: ts integrations [status|connect|disconnect|files] [google|microsoft] [essential|advanced]' : 'Uso: ts integracoes [status|conectar|desconectar|arquivos] [google|microsoft] [essencial|avancado]'));
2981
3107
  }
2982
3108
 
2983
3109
  function idioma(l) {
@@ -3046,6 +3172,7 @@ function recallCmd(args) {
3046
3172
  case 'logout': return logout();
3047
3173
  case 'chat': case 'conversa': return chatRepl();
3048
3174
  case 'imagem': case 'image': case 'img': return imageCmd(POS.slice(1));
3175
+ case 'gerar-video': case 'video-gerar': case 'generate-video': return generateVideoCmd(POS.slice(1));
3049
3176
  case 'quem': case 'whoami': return quem();
3050
3177
  case 'nova': case 'new': return nova();
3051
3178
  case 'run': return runCmd(POS.slice(1));
@@ -3082,6 +3209,7 @@ function recallCmd(args) {
3082
3209
  case 'doctor': case 'diagnostico-ambiente': case 'checkup': return doctorCmd();
3083
3210
  case 'privacidade': case 'privacy': return privacyCmd();
3084
3211
  case 'conta': case 'account': return accountCmd(POS.slice(1));
3212
+ case 'diagnosticos': case 'diagnostics': return diagnosticsCmd(POS.slice(1));
3085
3213
  case 'integracoes': case 'integrações': case 'integrations': return integracoesCmd(POS.slice(1));
3086
3214
  case 'meta': case 'missao': case 'mission': return metaCmd();
3087
3215
  case 'runs': return runsCmd();
@@ -3102,6 +3230,9 @@ function recallCmd(args) {
3102
3230
  if (/^\s*(?:(?:quero|preciso)\s+(?:que\s+)?(?:voc[eê]\s+)?|(?:por favor[,\s]+)?)?(?:gere|gerar|crie|criar|fa[çc]a|produza|desenhe|ilustre|edite)\s+(?:uma|a|minha)?\s*(?:imagem|foto|ilustra[çc][aã]o|arte|banner|capa|mockup)\b/i.test(text)) {
3103
3231
  return imageCmd([text]);
3104
3232
  }
3233
+ if (/^\s*(?:(?:quero|preciso)\s+(?:que\s+)?(?:voc[eê]\s+)?|(?:por favor[,\s]+)?)?(?:gere|gerar|crie|criar|fa[çc]a|produza)\s+(?:um|o|meu)?\s*(?:vídeo|video|clipe)\b/i.test(text)) {
3234
+ return generateVideoCmd([text]);
3235
+ }
3105
3236
  const r = await router.route(text, cfg.token);
3106
3237
  if (r.dest === 'agente') { console.log(' ' + C.cyan('⚙') + ' ' + C.dim(T.route_agent)); return agentCmd([text]); }
3107
3238
  if (r.dest === 'run') { console.log(' ' + C.indigo('◆') + ' ' + C.dim(T.route_run)); return runCmd([text]); }