terminal-smart-cli 0.97.1 → 0.97.3

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 CHANGED
@@ -27,6 +27,7 @@ ts "como libero a porta 443 no ufw?"
27
27
  | `ts agente "tarefa"` | **Executa DE VERDADE nesta máquina** — comandos, arquivos, diagnóstico. Destrutivo pede aprovação; com `--yes` só roda se você aprovar remotamente no Telegram, senão é recusado |
28
28
  | `ts agente --continuar "..."` | Retoma o trabalho anterior desta pasta |
29
29
  | `ts agente --ler` / `--plano` | Modo só-leitura (Ask) / propõe um plano sem agir |
30
+ | `ts imagem "descrição"` | Escolhe Premium, Econômica ou Sem imagem antes de consumir créditos |
30
31
  | `ts agente --worktree` | Isola a run num git worktree (não toca a árvore principal) |
31
32
  | `ts agente --stream-json` | Modo headless/CI: só NDJSON tipado no stdout |
32
33
  | `ts diagnosticar "erro"` | **Investiga a causa raiz**: hipótese → sonda (só-leitura) → verificação adversarial → veredito. `--remoto "ssh user@host"` investiga uma VPS |
package/bin/ts.js CHANGED
@@ -172,6 +172,97 @@ async function ensureConv(token) {
172
172
  cfg = config.save({ convId: r.id });
173
173
  return r.id;
174
174
  }
175
+
176
+ // ── ts imagem "descrição" ───────────────────────────────────────────────────
177
+ // A escolha é sempre explícita. --yes sozinho não autoriza uma geração paga:
178
+ // scripts precisam declarar --premium ou --economica.
179
+ async function imageCmd(args) {
180
+ const token = needToken();
181
+ const en = cfg.lang === 'en';
182
+ const prompt = args.join(' ').trim() || await readStdin();
183
+ if (!prompt || prompt.trim().length < 8) {
184
+ console.error(ui.errLine(en ? 'Describe the image (at least 8 characters).' : 'Descreva a imagem (mínimo de 8 caracteres).'));
185
+ process.exit(2);
186
+ }
187
+ const catalog = await api('/api/ia/image/options', { token });
188
+ const options = Array.isArray(catalog.options) ? catalog.options : [];
189
+ let choice = FLAGS.has('--premium') ? 'premium'
190
+ : (FLAGS.has('--economica') || FLAGS.has('--economy')) ? 'economy'
191
+ : (FLAGS.has('--sem-imagem') || FLAGS.has('--none')) ? 'none' : '';
192
+
193
+ if (!choice && (!process.stdin.isTTY || JSON_OUT)) {
194
+ throw new ApiError(en
195
+ ? 'Choose --premium, --economy or --none. Image generation is never silently approved.'
196
+ : 'Escolha --premium, --economica ou --sem-imagem. Geração de imagem nunca é aprovada silenciosamente.', { code: 'image_confirmation_required' });
197
+ }
198
+ if (!choice) {
199
+ const byId = id => options.find(o => o.id === id) || {};
200
+ const premium = byId('premium'), economy = byId('economy');
201
+ console.log('\n' + ui.box([
202
+ C.bold(en ? 'This action can generate an image' : 'Esta ação pode gerar uma imagem'),
203
+ C.dim(en ? 'Choose before spending credits. There is no silent paid fallback.' : 'Escolha antes de gastar créditos. Não existe fallback pago silencioso.'),
204
+ '',
205
+ ` ${C.cyan('1')} ${C.bold(premium.label || 'Premium · GPT Image 2')} ${C.dim('~' + (premium.estimatedCredits || '?') + ' créditos')}${premium.allowed === false ? C.warn(en ? ' · requires Pro/Ultra' : ' · requer Pro/Ultra') : ''}`,
206
+ ` ${C.cyan('2')} ${C.bold(economy.label || 'Econômica · Gemini')} ${C.dim('~' + (economy.estimatedCredits || '?') + ' créditos')}`,
207
+ ` ${C.cyan('3')} ${C.bold(en ? 'Continue without images' : 'Continuar sem imagens')} ${C.dim(en ? 'no charge' : 'sem cobrança')}`,
208
+ '',
209
+ C.dim((en ? 'Plan ' : 'Plano ') + (catalog.plan || '?') + ' · ' + (en ? 'balance ' : 'saldo ') + (catalog.remaining ?? 0)),
210
+ ], { title: 'ts imagem' }));
211
+ const answer = String(await ui.ask(C.dim(en ? ' Choose 1, 2 or 3 › ' : ' Escolha 1, 2 ou 3 › '))).trim();
212
+ choice = answer === '1' ? 'premium' : answer === '2' ? 'economy' : 'none';
213
+ }
214
+ const selected = options.find(o => o.id === choice);
215
+ if (!selected || selected.allowed === false) {
216
+ throw new ApiError((selected && selected.upgradeRequired)
217
+ ? (en ? 'GPT Image 2 requires Pro or Ultra.' : 'GPT Image 2 requer plano Pro ou Ultra.')
218
+ : (en ? 'Invalid image option.' : 'Opção de imagem inválida.'), { code: 'plan_limit' });
219
+ }
220
+ if (choice === 'none') {
221
+ const result = { ok: true, skipped: true, charged: 0, message: en ? 'No image generated and no credits charged.' : 'Nenhuma imagem foi gerada e nenhum crédito foi cobrado.' };
222
+ if (JSON_OUT) console.log(JSON.stringify(result)); else console.log('\n' + ui.infoLine(result.message));
223
+ return;
224
+ }
225
+ const convId = await ensureConv(token);
226
+ const sp = JSON_OUT ? { stop() {} } : ui.spinner(en ? 'generating image…' : 'gerando imagem…').start();
227
+ let result;
228
+ try {
229
+ result = await api('/api/ia/image', {
230
+ method: 'POST',
231
+ token,
232
+ timeoutMs: 200000,
233
+ body: {
234
+ conversationId: convId,
235
+ prompt,
236
+ choice,
237
+ quality: selected.quality,
238
+ size: selected.size,
239
+ confirmed: true,
240
+ },
241
+ });
242
+ } finally { sp.stop(); }
243
+ if (!result || !result.url) throw new Error(en ? 'Image provider returned no image.' : 'O provedor não retornou uma imagem.');
244
+ const fs = require('fs'), os = require('os'), path = require('path');
245
+ let bytes, ext = '.png';
246
+ const dataMatch = String(result.url).match(/^data:image\/(png|jpeg|webp);base64,([\s\S]+)$/);
247
+ if (dataMatch) {
248
+ ext = dataMatch[1] === 'jpeg' ? '.jpg' : '.' + dataMatch[1];
249
+ bytes = Buffer.from(dataMatch[2], 'base64');
250
+ } else {
251
+ const downloaded = await fetch(result.url, { signal: AbortSignal.timeout(60000) });
252
+ if (!downloaded.ok) throw new Error('Download da imagem falhou: HTTP ' + downloaded.status);
253
+ bytes = Buffer.from(await downloaded.arrayBuffer());
254
+ const type = downloaded.headers.get('content-type') || '';
255
+ ext = /jpeg/.test(type) ? '.jpg' : /webp/.test(type) ? '.webp' : '.png';
256
+ }
257
+ if (!bytes || bytes.length < 256 || bytes.length > 25 * 1024 * 1024) throw new Error(en ? 'Invalid image payload.' : 'Conteúdo de imagem inválido.');
258
+ const target = path.join(os.homedir(), 'Downloads', `terminal-smart-imagem-${Date.now()}${ext}`);
259
+ const partial = target + '.partial-' + process.pid;
260
+ fs.writeFileSync(partial, bytes, { flag: 'wx' });
261
+ fs.renameSync(partial, target);
262
+ const output = { ok: true, path: target, model: result.model, charged: result.charged, remaining: result.remaining, choice };
263
+ if (JSON_OUT) console.log(JSON.stringify(output));
264
+ 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}`));
265
+ }
175
266
  // Envia uma mensagem na conversa ativa e devolve o texto (recria a conversa 1x se apagada na web).
176
267
  async function sendMessage(token, content) {
177
268
  const doStream = async (convId) => {
@@ -2757,6 +2848,7 @@ function recallCmd(args) {
2757
2848
  case 'login': return login();
2758
2849
  case 'logout': return logout();
2759
2850
  case 'chat': case 'conversa': return chatRepl();
2851
+ case 'imagem': case 'image': case 'img': return imageCmd(POS.slice(1));
2760
2852
  case 'quem': case 'whoami': return quem();
2761
2853
  case 'nova': case 'new': return nova();
2762
2854
  case 'run': return runCmd(POS.slice(1));
@@ -2806,6 +2898,9 @@ function recallCmd(args) {
2806
2898
  // pipe/--json ficam determinísticos no chat (scripts não levam surpresa)
2807
2899
  const text = POS.join(' ');
2808
2900
  if (process.stdin.isTTY && !JSON_OUT && cfg.token) {
2901
+ 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)) {
2902
+ return imageCmd([text]);
2903
+ }
2809
2904
  const r = await router.route(text, cfg.token);
2810
2905
  if (r.dest === 'agente') { console.log(' ' + C.cyan('⚙') + ' ' + C.dim(T.route_agent)); return agentCmd([text]); }
2811
2906
  if (r.dest === 'run') { console.log(' ' + C.indigo('◆') + ' ' + C.dim(T.route_run)); return runCmd([text]); }
package/lib/agent.js CHANGED
@@ -148,7 +148,7 @@ async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools =
148
148
  throw new ApiError(_em, { status: res.status, code: (j && j.code) || (_cap ? 'no_credits' : '') });
149
149
  }
150
150
  const ch = (j.choices && j.choices[0]) || {};
151
- return { msg: ch.message || { content: '' }, usage: j.usage || {}, model: j.model || 'smart' };
151
+ return { msg: ch.message || { content: '' }, usage: j.usage || {}, model: j.model || 'smart', billing: j.ts_billing || null };
152
152
  }, { tries: 4, baseMs: 800, onRetry });
153
153
  }
154
154
 
@@ -202,13 +202,17 @@ async function _subAgent({ task, k, model, cwd, lang, onStep }) {
202
202
  : 'You are a READ-ONLY exploration SUB-AGENT. Investigate by reading/listing/searching files and return a SHORT objective summary with the facts and paths that matter. Do NOT edit/create/run anything. When you have the answer, write the summary as text (no more tool calls).')
203
203
  + `\nDIRETÓRIO DE TRABALHO: ${cwd}`;
204
204
  let msgs = [{ role: 'system', content: sys }, { role: 'user', content: String(task || '') }];
205
- let out = '', tin = 0, tout = 0, tcach = 0;
206
- const _acc = (u) => { tin += u.prompt_tokens || 0; tout += u.completion_tokens || 0; tcach += (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || u.cached_tokens || 0; };
205
+ let out = '', tin = 0, tout = 0, tcach = 0, billed = 0;
206
+ const _acc = (u, billing) => {
207
+ tin += u.prompt_tokens || 0; tout += u.completion_tokens || 0;
208
+ tcach += (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || u.cached_tokens || 0;
209
+ billed += (billing && billing.charged) || 0;
210
+ };
207
211
  for (let i = 0; i < 8; i++) {
208
212
  let r;
209
213
  try { r = await llm({ baseUrl: k.baseUrl, key: k.key, messages: msgs, model, toolsOverride: roTools }); }
210
214
  catch (e) { if (e instanceof ApiError && (e.code === 'no_credits' || e.status === 402)) throw e; break; } // teto de IA → propaga (CTA); transitório → para
211
- _acc(r.usage || {});
215
+ _acc(r.usage || {}, r.billing);
212
216
  const tcs = r.msg.tool_calls || [];
213
217
  if (!tcs.length) { out = String(r.msg.content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim(); break; }
214
218
  msgs.push({ role: 'assistant', content: r.msg.content || '', tool_calls: tcs });
@@ -226,11 +230,11 @@ async function _subAgent({ task, k, model, cwd, lang, onStep }) {
226
230
  const pedido = lang !== 'en' ? 'PARE de usar ferramentas. Escreva agora um RESUMO curto do que você descobriu até aqui (fatos e caminhos).' : 'STOP using tools. Write a SHORT summary of what you found so far (facts and paths).';
227
231
  try {
228
232
  const r = await llm({ baseUrl: k.baseUrl, key: k.key, messages: [...msgs, { role: 'user', content: pedido }], model, noTools: true, signalMs: 60000 });
229
- _acc(r.usage || {});
233
+ _acc(r.usage || {}, r.billing);
230
234
  out = String(r.msg.content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
231
235
  } catch (_) {}
232
236
  }
233
- return { resumo: out || (lang !== 'en' ? '(o sub-agente não produziu um resumo)' : '(sub-agent produced no summary)'), _tin: tin, _tout: tout, _tcach: tcach };
237
+ return { resumo: out || (lang !== 'en' ? '(o sub-agente não produziu um resumo)' : '(sub-agent produced no summary)'), _tin: tin, _tout: tout, _tcach: tcach, _billed: billed };
234
238
  }
235
239
 
236
240
  // Aprovação remota via Telegram (modo --yes): envia o pedido e faz poll até
@@ -536,6 +540,7 @@ async function run(task, opts = {}) {
536
540
  const r = await llm({ baseUrl: k.baseUrl, key: k.key, model: selectedModel, noTools: true, signalMs: 60000, messages: [{ role: 'system', content: sys }, { role: 'user', content: lines }] });
537
541
  const u = r.usage || {};
538
542
  acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
543
+ charged += (r.billing && r.billing.charged) || 0;
539
544
  resumo = String(r.msg.content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim().slice(0, 6000);
540
545
  } catch (_) {}
541
546
  if (!resumo) resumo = lang !== 'en'
@@ -551,6 +556,7 @@ async function run(task, opts = {}) {
551
556
  // cobrança Smart Credits (best-effort — o teto da sk-hub já protege no gateway)
552
557
  const _bill = async () => {
553
558
  if (acc.inTok + acc.outTok <= 0) return;
559
+ if (k.billingAuthoritative) return;
554
560
  try {
555
561
  const c = await api('/api/credit/charge', { method: 'POST', token, body: { model: usedModel, inTok: acc.inTok, outTok: acc.outTok, cachedTok: acc.cachedTok } });
556
562
  charged = (c && c.charged) || 0;
@@ -581,6 +587,7 @@ async function run(task, opts = {}) {
581
587
  }
582
588
  const u = r.usage || {};
583
589
  acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
590
+ charged += (r.billing && r.billing.charged) || 0;
584
591
  acc.cachedTok += (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || u.cached_tokens || 0;
585
592
  lastCtx = { used: u.prompt_tokens || estMsgsTok(messages), window: ctxWindow };
586
593
  if (r.model) usedModel = r.model;
@@ -866,6 +873,7 @@ async function run(task, opts = {}) {
866
873
  onStep({ name: 'explorar', detail: argsShort(name, input) });
867
874
  const sub = await _subAgent({ task: input.tarefa || input.pergunta || '', k, model: selectedModel, cwd, lang, onStep });
868
875
  acc.inTok += sub._tin; acc.outTok += sub._tout; acc.cachedTok += sub._tcach || 0;
876
+ charged += sub._billed || 0;
869
877
  result = { resumo: sub.resumo };
870
878
  steps++; _ran = true;
871
879
  }
@@ -951,6 +959,7 @@ async function run(task, opts = {}) {
951
959
  const r = await llm({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs: 60000 });
952
960
  const u = r.usage || {};
953
961
  acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
962
+ charged += (r.billing && r.billing.charged) || 0;
954
963
  acc.cachedTok += (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || u.cached_tokens || 0;
955
964
  if (r.model) usedModel = r.model;
956
965
  finalText = String(r.msg.content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').replace(/<think>[\s\S]*$/i, '').trim();
package/lib/i18n.js CHANGED
@@ -8,6 +8,7 @@ const STR = {
8
8
  ['ts', 'abre o modo conversa — digite naturalmente'],
9
9
  ['ts "pergunta"', 'resposta única e volta pro shell'],
10
10
  ['cat log | ts "analise"', 'analisa o que vier do pipe'],
11
+ ['ts imagem "descrição"', 'gera imagem com escolha Premium/Econômica/Sem imagem antes da cobrança'],
11
12
  ['ts video <url> "..."', 'lê a transcrição de um vídeo e responde (yt-dlp)'],
12
13
  ['ts arquivar <url>', 'salva a página como 1 HTML offline (monolith)'],
13
14
  ['ts qr', 'QR pra continuar a conversa no celular'],
@@ -189,6 +190,7 @@ const STR = {
189
190
  ['ts', 'opens chat mode — just type naturally'],
190
191
  ['ts "question"', 'one-shot answer, back to the shell'],
191
192
  ['cat log | ts "analyze"', 'analyze piped input'],
193
+ ['ts image "description"', 'choose Premium/Economy/No image before any charge'],
192
194
  ['ts video <url> "..."', 'reads a video transcript and answers (yt-dlp)'],
193
195
  ['ts arquivar <url>', 'saves the page as 1 offline HTML (monolith)'],
194
196
  ['ts qr', 'QR to continue the conversation on your phone'],
@@ -9,7 +9,7 @@
9
9
  const crypto = require('crypto');
10
10
 
11
11
  const INTELLIGENCE_CONTRACT = 1;
12
- const PRICE_REVISION = '2026-07-26';
12
+ const PRICE_REVISION = '2026-07-28';
13
13
 
14
14
  const MODEL_CATALOG = Object.freeze({
15
15
  smart: {
@@ -18,7 +18,7 @@ const MODEL_CATALOG = Object.freeze({
18
18
  },
19
19
  'deepseek-v4-flash': {
20
20
  upstreamId: 'deepseek/deepseek-v4-flash',
21
- provider: 'deepseek', contextWindow: 1048576, price: { input: 0.14, output: 0.28, cachedInput: 0.028 },
21
+ provider: 'deepseek', contextWindow: 1048576, price: { input: 0.14, output: 0.28, cachedInput: 0.0028 },
22
22
  capabilities: ['tools', 'code', 'planning', 'execution', 'long-context'], toolReliability: 0.94, quality: 0.86,
23
23
  },
24
24
  'deepseek-chat': {
@@ -51,11 +51,21 @@ const MODEL_CATALOG = Object.freeze({
51
51
  provider: 'moonshot', contextWindow: 262144, price: { input: 0.75, output: 3.50, cachedInput: 0.15 },
52
52
  capabilities: ['tools', 'code', 'vision', 'long-context'], toolReliability: 0.86, quality: 0.84,
53
53
  },
54
+ 'kimi-k3': {
55
+ upstreamId: 'moonshotai/kimi-k3',
56
+ provider: 'moonshot', contextWindow: 262144, price: { input: 3.00, output: 15.00, cachedInput: 0.30 },
57
+ capabilities: ['tools', 'code', 'planning', 'long-context'], toolReliability: 0.88, quality: 0.91,
58
+ },
54
59
  'glm-4.6': {
55
60
  upstreamId: 'z-ai/glm-4.6',
56
61
  provider: 'zai', contextWindow: 204800, price: { input: 0.50, output: 2.00, cachedInput: 0.10 },
57
62
  capabilities: ['tools', 'planning', 'code'], toolReliability: 0.84, quality: 0.82,
58
63
  },
64
+ 'glm-5.2': {
65
+ upstreamId: 'z-ai/glm-5.2',
66
+ provider: 'zai', contextWindow: 200000, price: { input: 0.7644, output: 2.4024, cachedInput: 0.15288 },
67
+ capabilities: ['tools', 'planning', 'code', 'long-context'], toolReliability: 0.88, quality: 0.89,
68
+ },
59
69
  'mimo-v2.5': {
60
70
  upstreamId: 'xiaomi/mimo-v2.5',
61
71
  provider: 'xiaomi', contextWindow: 1050000, price: { input: 0.14, output: 0.28, cachedInput: 0.0028 },
@@ -66,6 +76,26 @@ const MODEL_CATALOG = Object.freeze({
66
76
  provider: 'openai', contextWindow: 128000, price: { input: 0.15, output: 0.60, cachedInput: 0.075 },
67
77
  capabilities: ['tools', 'classification', 'summarization', 'review'], toolReliability: 0.88, quality: 0.76,
68
78
  },
79
+ 'gpt-5.6-sol': {
80
+ upstreamId: 'openai/gpt-5.6-sol',
81
+ provider: 'openai', contextWindow: 1000000, price: { input: 5.00, output: 30.00, cachedInput: 0.50 },
82
+ capabilities: ['tools', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.97, quality: 0.98,
83
+ },
84
+ 'gpt-5.6-terra': {
85
+ upstreamId: 'openai/gpt-5.6-terra',
86
+ provider: 'openai', contextWindow: 1000000, price: { input: 2.50, output: 15.00, cachedInput: 0.25 },
87
+ capabilities: ['tools', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.96, quality: 0.95,
88
+ },
89
+ 'gpt-5.6-luna': {
90
+ upstreamId: 'openai/gpt-5.6-luna',
91
+ provider: 'openai', contextWindow: 1000000, price: { input: 1.00, output: 6.00, cachedInput: 0.10 },
92
+ capabilities: ['tools', 'code', 'summarization', 'review', 'long-context'], toolReliability: 0.93, quality: 0.89,
93
+ },
94
+ 'claude-sonnet-5': {
95
+ upstreamId: 'anthropic/claude-sonnet-5',
96
+ provider: 'anthropic', contextWindow: 1000000, price: { input: 2.00, output: 10.00, cachedInput: 0.20 },
97
+ capabilities: ['tools', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.97, quality: 0.96,
98
+ },
69
99
  'claude-sonnet-4-6': {
70
100
  upstreamId: 'anthropic/claude-sonnet-4.6',
71
101
  provider: 'anthropic', contextWindow: 1000000, price: { input: 3.00, output: 15.00, cachedInput: 0.30 },
@@ -123,17 +153,24 @@ function normalizeModelId(model) {
123
153
  return upstream ? upstream[0] : raw;
124
154
  }
125
155
 
126
- function modelInfo(model) {
156
+ function modelInfo(model, now) {
127
157
  const id = normalizeModelId(model);
128
- return Object.assign({ id, known: !!MODEL_CATALOG[id] }, MODEL_CATALOG[id] || {
158
+ const info = MODEL_CATALOG[id] || {
129
159
  provider: 'unknown', contextWindow: 100000, price: DEFAULT_PRICE,
130
160
  capabilities: [], toolReliability: 0.50, quality: 0.50,
131
- });
161
+ };
162
+ const result = Object.assign({ id, known: !!MODEL_CATALOG[id] }, info);
163
+ const at = now ? new Date(now) : new Date();
164
+ if (id === 'claude-sonnet-5' && at.getTime() >= Date.parse('2026-09-01T00:00:00Z')) {
165
+ result.price = { input: 3.00, output: 15.00, cachedInput: 0.30 };
166
+ }
167
+ return result;
132
168
  }
133
169
 
134
- function legacyPriceMap() {
170
+ function legacyPriceMap(now) {
135
171
  const out = {};
136
- for (const [id, m] of Object.entries(MODEL_CATALOG)) {
172
+ for (const id of Object.keys(MODEL_CATALOG)) {
173
+ const m = modelInfo(id, now);
137
174
  out[id] = { i: m.price.input, o: m.price.output, c: m.price.cachedInput };
138
175
  }
139
176
  out._default = { i: DEFAULT_PRICE.input, o: DEFAULT_PRICE.output, c: DEFAULT_PRICE.cachedInput };
package/lib/meta.js CHANGED
@@ -611,8 +611,8 @@ async function _llmVision({ token, model, text, imageB64, images, maxTokens = 70
611
611
  const j = await res.json().catch(() => null);
612
612
  if (!res.ok) _capGuard(res, j);
613
613
  const u = j?.usage || {};
614
- let credits = 0;
615
- if (u.prompt_tokens) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
614
+ let credits = (j?.ts_billing && j.ts_billing.charged) || 0;
615
+ if (u.prompt_tokens && !_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
616
616
  return { text: String(j?.choices?.[0]?.message?.content || '').trim(), credits };
617
617
  }
618
618
  const _os = require('os');
@@ -811,9 +811,9 @@ async function _llmJson({ token, system, user, maxTokens = 900 }) {
811
811
  const j = await res.json().catch(() => null);
812
812
  if (!res.ok) _capGuard(res, j);
813
813
  const u = j?.usage || {};
814
- let credits = 0;
814
+ let credits = (j?.ts_billing && j.ts_billing.charged) || 0;
815
815
  if (u.prompt_tokens) {
816
- try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model: 'gemini-2.5-flash-lite', inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {}
816
+ if (!_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model: 'gemini-2.5-flash-lite', inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
817
817
  }
818
818
  return { json: _tolerantJson(String(j?.choices?.[0]?.message?.content || '')), credits };
819
819
  }
@@ -834,8 +834,8 @@ async function _llmText({ token, model, system, user, maxTokens = 1200 }) {
834
834
  const j = await res.json().catch(() => null);
835
835
  if (!res.ok) _capGuard(res, j);
836
836
  const u = j?.usage || {};
837
- let credits = 0;
838
- if (u.prompt_tokens) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
837
+ let credits = (j?.ts_billing && j.ts_billing.charged) || 0;
838
+ if (u.prompt_tokens && !_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
839
839
  return { text: String(j?.choices?.[0]?.message?.content || '').trim(), credits };
840
840
  }
841
841
 
package/lib/router.js CHANGED
@@ -79,7 +79,7 @@ async function classify(msg, token) {
79
79
  const word = String(j?.choices?.[0]?.message?.content || '').toLowerCase();
80
80
  // cobra a classificação (mesmo padrão do web-intent) — best-effort
81
81
  const u = j?.usage || {};
82
- if (u.prompt_tokens) api('/api/credit/charge', { method: 'POST', token, body: { model: 'gemini-2.5-flash-lite', inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }).catch(() => {});
82
+ if (u.prompt_tokens && !k.billingAuthoritative) api('/api/credit/charge', { method: 'POST', token, body: { model: 'gemini-2.5-flash-lite', inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }).catch(() => {});
83
83
  if (word.includes('agente')) return 'agente';
84
84
  if (word.includes('orquestrar')) return 'run';
85
85
  return 'chat';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.97.1",
3
+ "version": "0.97.3",
4
4
  "description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
5
5
  "bin": {
6
6
  "ts": "bin/ts.js"