terminal-smart-cli 0.98.0 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/i18n.js CHANGED
@@ -14,6 +14,14 @@ const STR = {
14
14
  ['ts qr', 'QR pra continuar a conversa no celular'],
15
15
  ['ts nova', 'zera o contexto da conversa'],
16
16
  ] },
17
+ { title: 'Hub (celular pareado)', items: [
18
+ ['ts hub iniciar', 'inicia o hub: conexão de saída com o relay (deixe rodando)'],
19
+ ['ts hub parear', 'QR de uso único para parear o celular; você confirma aqui no PC'],
20
+ ['ts hub aparelhos', 'lista os aparelhos pareados'],
21
+ ['ts hub revogar <id>', 'revoga um aparelho e corta a sessão dele na hora'],
22
+ ['ts hub status', 'estado do hub (relay, aparelhos, Parar tudo)'],
23
+ ['ts hub exportar-telas', 'grava as telas do tmux (janelas permitidas, com máscara de segredos) para o hub ler'],
24
+ ] },
17
25
  { title: 'Galeria de Skills (comunidade)', items: [
18
26
  ['ts skills', 'lista as skills da comunidade (mais instaladas)'],
19
27
  ['ts skills ver <slug>', 'lê a skill INTEIRA antes de instalar'],
@@ -39,12 +47,12 @@ const STR = {
39
47
  { title: 'Agente LOCAL (mãos nesta máquina)', items: [
40
48
  ['ts agente "tarefa"', 'executa DE VERDADE aqui: comandos, arquivos, diagnóstico'],
41
49
  ['ts agente "..." --ferramentas a,b', 'ESCOPO ESTRITO: somente as ferramentas autorizadas entram na missão'],
42
- ['ts agente "..." --yes', 'autônomo (destrutivo pede aprovação no Telegram)'],
50
+ ['ts agente "..." --yes', 'autônomo (o que exige aprovação é recusado — não há quem aprove no terminal)'],
43
51
  ['ts diagnosticar "erro"', 'INVESTIGA a causa raiz: hipótese→sonda→veredito (só-leitura)'],
44
52
  ['ts diagnosticar "..." --remoto "ssh user@host"', 'investiga uma máquina remota'],
45
53
  ['ts diagnosticar "..." --registrar', 'salva a investigação como nota em .ts-conhecimento (inclusive o que foi REFUTADO)'],
46
- ['ts sentinela add "nome" --cmd "..."', 'VIGIA determinístico (regra do if, ZERO token); --escalar diagnostica se falhar; --autofix conserta (com aprovação no Telegram)'],
47
- ['ts sentinela instalar', 'agenda os checks no SO (cron/Task Scheduler) e avisa no Telegram'],
54
+ ['ts sentinela add "nome" --cmd "..."', 'VIGIA determinístico (regra do if, ZERO token); --escalar diagnostica se falhar; --autofix propõe o conserto e só aplica com sua aprovação no terminal'],
55
+ ['ts sentinela instalar', 'agenda os checks no SO (cron/Task Scheduler)'],
48
56
  ['ts agente --continuar "..."', 'retoma o trabalho anterior desta pasta'],
49
57
  ['ts agente "..." --yolo', 'FULL-AUTO: aprova TUDO na sessão, inclusive destrutivos (o gate anti-catástrofe ~/.ts/máquina segue ativo)'],
50
58
  ['ts agente "..." --navegador', 'EXPERIMENTAL: dá um Chrome real ao agente (abrir/ler/clicar/print+visão)'],
@@ -63,8 +71,10 @@ const STR = {
63
71
  ['ts politica testar "rm -rf build"', 'mostra o veredito e DE QUAL regra veio'],
64
72
  ['ts checkpoints', 'o que o agente mudou em cada sessão'],
65
73
  ['ts desfazer --sessao [id]', 'DESFAZ TUDO o que o agente fez na sessão (pula o que você editou depois)'],
66
- ['ts conectar', 'usa SUA chave de IA (NVIDIA/Google/Cerebras/Groq/Ollama) em vez dos créditos'],
67
- ['ts conectar nuvem', 'volta a usar os créditos do TS'],
74
+ ['ts conectar', 'conecta SUA chave de IA (NVIDIA/Google/Cerebras/Groq/OpenRouter) ou o Ollama local — o agente roda via sua chave'],
75
+ ['ts ia conectar chatgpt|claude', 'login oficial do Codex/Claude Code NESTE PC — chat e agente rodam via sua assinatura'],
76
+ ['ts ia status · ts ia usar chatgpt|claude', 'mostra/escolhe a IA da sua conta neste PC (automatico volta para a sua chave)'],
77
+ ['ts conectar nuvem', 'desativa a chave própria (volta à IA da conta: sua assinatura ou os créditos legados que restarem)'],
68
78
  ] },
69
79
  { title: 'Agentes na nuvem (orquestração)', items: [
70
80
  ['ts run "objetivo"', 'planeja → você aprova → executa'],
@@ -74,7 +84,7 @@ const STR = {
74
84
  ] },
75
85
  { title: 'Conta', items: [
76
86
  ['ts login · ts logout', 'conecta / desconecta este terminal'],
77
- ['ts uso', 'créditos e consumo do mês'],
87
+ ['ts uso', 'plano, IA em uso e créditos legados (se houver)'],
78
88
  ['ts quem', 'conta conectada'],
79
89
  ['ts conta exportar', 'baixa seus dados sem senhas, tokens ou chaves privadas'],
80
90
  ['ts conta abrir', 'abre Minha Conta para privacidade e exclusão segura'],
@@ -87,7 +97,7 @@ const STR = {
87
97
  ['ts idioma pt|en', 'idioma (padrão pt-BR)'],
88
98
  ] },
89
99
  ],
90
- help_footer: 'Cada uso debita dos seus Smart Credits — acompanhe com `ts uso`.',
100
+ help_footer: 'A IA roda via sua chave (`ts conectar`), via sua assinatura (`ts ia conectar`) ou via Ollama local — veja qual está em uso com `ts uso`.',
91
101
  repl_hello: 'converse naturalmente — eu identifico se é pergunta, ação ou objetivo · /ajuda · sair',
92
102
  onboard_title: 'Primeira vez? É só conversar — sem comando nenhum.',
93
103
  onboard_lines: [
@@ -99,7 +109,7 @@ const STR = {
99
109
  route_agent: 'isso pede ações nesta máquina — agindo como agente (digite /chat antes da frase pra só conversar)',
100
110
  route_run: 'objetivo grande — planejando com a equipe de agentes (você aprova antes de gastar)',
101
111
  repl_account: (u, p) => `conta ${u} · plano ${p}`,
102
- repl_credits: (c) => `${c} créditos`,
112
+ repl_credits: (c) => `${c} créditos legados`,
103
113
  repl_help: [
104
114
  ['digite qualquer coisa', 'conversa em linguagem natural'],
105
115
  ['/agente "tarefa"', 'executa DE VERDADE nesta máquina (comandos, arquivos)'],
@@ -110,7 +120,7 @@ const STR = {
110
120
  ['/custo', 'quanto está gastando (ou de quem é a conta)'],
111
121
  ['/tema [nome]', 'troca a paleta do terminal'],
112
122
  ['/limpar', 'limpa a tela (o contexto continua)'],
113
- ['/uso', 'créditos do mês'],
123
+ ['/uso', 'plano e IA em uso'],
114
124
  ['/nova', 'zera o contexto'],
115
125
  ['sair', 'encerra'],
116
126
  ],
@@ -143,11 +153,17 @@ const STR = {
143
153
  run_paused: (r) => `Pausada (${r}). Retome pela web ou app.`,
144
154
  runs_title: 'Últimas orquestrações',
145
155
  runs_empty: 'Nenhuma orquestração ainda. Comece com: ts run "objetivo"',
146
- uso_title: 'Uso do mês',
156
+ uso_title: 'Uso e IA da conta',
147
157
  uso_plan: 'Plano',
158
+ uso_ai: 'IA em uso',
159
+ uso_ai_none: 'nenhuma IA própria conectada — use `ts conectar <provedor>` ou `ts ia conectar chatgpt|claude`',
160
+ uso_ai_unknown: 'não consegui confirmar a IA da conta agora (sem resposta do servidor) — veja depois com `ts uso` ou `ts ia status`',
161
+ uso_ai_sub_disconnected: (label, prov) => `modo ${label} selecionado, mas a CLI da conta não está pronta neste PC — o chat e o agente não rodam assim; faça o login com \`ts ia conectar ${prov}\` ou volte com \`ts ia usar automatico\``,
162
+ uso_ai_sub_not_selected: (prov) => `conta logada neste PC, mas não selecionada — use \`ts ia usar ${prov}\``,
163
+ byok_no_balance: (prov) => `Sua chave própria do ${prov} recusou a chamada por saldo ou limite no provedor. Nada foi cobrado pelo Terminal Smart. Adicione saldo no provedor, troque de provedor com \`ts conectar <provedor>\` (ou Ollama local) ou use sua assinatura com \`ts ia conectar chatgpt|claude\`.`,
148
164
  uso_remaining: 'restantes',
149
165
  uso_unlimited: 'ilimitado',
150
- uso_credits: 'créditos',
166
+ uso_credits: 'créditos legados',
151
167
  uso_last: 'Últimos usos',
152
168
  quem_title: 'Conta conectada',
153
169
  quem_user: 'Usuário',
@@ -158,18 +174,21 @@ const STR = {
158
174
  lang_invalid: 'Idioma não suportado. Use: ts idioma pt ou ts idioma en',
159
175
  err_conn: 'Não consegui falar com o servidor. Verifique sua internet e tente de novo.',
160
176
  err_generic: 'Algo deu errado',
161
- no_credits: 'Seu limite de IA acabou. O Free renova diariamente; planos pagos renovam por ciclo. Continue agora em terminalsmart.com.br/planos',
177
+ no_credits: 'A IA do Terminal Smart não está disponível para esta execução (sem créditos legados). Use a sua: `ts conectar <provedor>` (sua chave ou Ollama local) ou `ts ia conectar chatgpt|claude` (sua assinatura).',
162
178
  ask_empty: 'Diga o que você precisa. Ex.: ts "como liberar a porta 443 no ufw?"',
163
179
  chat_hint: 'digite sua mensagem · "sair" encerra · "/nova" zera o contexto',
164
180
  chat_prompt: 'Você › ',
165
- chat_bye: 'Até logo! A conversa continua salva na web (/ia).',
181
+ chat_bye: 'Até logo! O histórico desta pasta fica salvo só neste PC (`ts nova` limpa).',
166
182
  agent_need: 'Descreva a tarefa. Ex.: ts agente "libere espaço em disco analisando as pastas maiores"',
167
183
  agent_thinking: 'agente pensando',
168
184
  agent_blocked: 'bloqueado pelo harness',
169
185
  agent_approve: (cmd) => `O agente quer rodar um comando DESTRUTIVO:\n\n ${cmd}\n\n Permitir? (s/N) `,
170
- agent_footer: (st, cr, s) => `${st} passo(s) · ${cr} créditos · ${s}`,
171
- agent_footer_byok: (st, prov, s) => `${st} passo(s) · sua chave (${prov}) · ${s}`,
172
- agent_remote_wait: (ttl) => `comando destrutivo — pedido de aprovação enviado no seu Telegram (responda em até ${Math.round(ttl / 60)} min; sem resposta = negado)`,
186
+ agent_footer: (st, cr, s) => `${st} passo(s) · ${cr} créditos legados · ${s}`,
187
+ agent_footer_byok: (st, prov, s) => `${st} passo(s) · via sua chave (${prov}) · ${s}`,
188
+ agent_footer_via: (st, via, s) => `${st} passo(s) · ${via} · ${s}`,
189
+ via_key: (prov) => `via sua chave (${prov})`,
190
+ via_subscription: 'via sua assinatura',
191
+ via_ollama: 'via Ollama local',
173
192
  meta_need: 'Descreva o objetivo. Ex.: ts meta "monte um site de portfólio completo nesta pasta" --budget 300',
174
193
  meta_title: 'MISSÃO',
175
194
  meta_exists: (g) => `Já existe uma missão em andamento neste diretório ("${g.slice(0, 60)}…"). Rode "ts meta" sem objetivo pra RETOMAR, ou use --novo pra recomeçar do zero.`,
@@ -203,6 +222,14 @@ const STR = {
203
222
  ['ts qr', 'QR to continue the conversation on your phone'],
204
223
  ['ts nova', 'reset the conversation context'],
205
224
  ] },
225
+ { title: 'Hub (paired phone)', items: [
226
+ ['ts hub iniciar', 'start the hub: outbound connection to the relay (keep it running)'],
227
+ ['ts hub parear', 'one-time QR to pair your phone; you confirm here on the PC'],
228
+ ['ts hub aparelhos', 'list paired devices'],
229
+ ['ts hub revogar <id>', 'revoke a device and cut its session right away'],
230
+ ['ts hub status', 'hub status (relay, devices, Stop all)'],
231
+ ['ts hub exportar-telas', 'write tmux screens (allowed windows, secrets masked) for the hub to read'],
232
+ ] },
206
233
  { title: 'Skills gallery (community)', items: [
207
234
  ['ts skills', 'list community skills (most installed)'],
208
235
  ['ts skills ver <slug>', 'read the WHOLE skill before installing'],
@@ -220,12 +247,12 @@ const STR = {
220
247
  { title: 'LOCAL agent (hands on this machine)', items: [
221
248
  ['ts agente "task"', 'actually does it here: commands, files, diagnosis'],
222
249
  ['ts agente "..." --tools a,b', 'STRICT SCOPE: only authorized tools enter the mission'],
223
- ['ts agente "..." --yes', 'autonomous (destructive asks on Telegram)'],
250
+ ['ts agente "..." --yes', 'autonomous (anything that needs approval is refused — nobody at the terminal to approve)'],
224
251
  ['ts agente --continuar "..."', 'resume this folder\'s previous work'],
225
252
  ['ts agente "..." --yolo', 'FULL-AUTO: approves EVERYTHING this session, destructive included (~/.ts & machine anti-catastrophe still on)'],
226
253
  ['ts agente "..." --navegador', 'EXPERIMENTAL: gives the agent a real Chrome (open/read/click/print+vision)'],
227
254
  ['ts sentinela add "name" --cmd "..."', 'DETERMINISTIC watch (if-rule, ZERO tokens); escalates to AI only on failure'],
228
- ['ts sentinela instalar', 'schedule checks on the OS (cron/Task Scheduler), alert on Telegram'],
255
+ ['ts sentinela instalar', 'schedule checks on the OS (cron/Task Scheduler)'],
229
256
  ['ts meta "big goal"', 'MISSION: checklist + rounds until done (overnight)'],
230
257
  ['ts meta --status', 'mission state for this directory'],
231
258
  ['ts eval suite.json', 'grade the agent on a case suite (AI judge + score)'],
@@ -235,8 +262,10 @@ const STR = {
235
262
  ['ts politica testar "rm -rf build"', 'shows the verdict and WHICH rule produced it'],
236
263
  ['ts checkpoints', 'what the agent changed in each session'],
237
264
  ['ts desfazer --sessao [id]', 'UNDO everything the agent did in that session (skips what you edited after)'],
238
- ['ts conectar', 'use YOUR own AI key (NVIDIA/Google/Cerebras/Groq/Ollama) instead of credits'],
239
- ['ts conectar nuvem', 'switch back to TS credits'],
265
+ ['ts conectar', 'connect YOUR AI key (NVIDIA/Google/Cerebras/Groq/OpenRouter) or local Ollama — the agent runs via your key'],
266
+ ['ts ia conectar chatgpt|claude', 'official Codex/Claude Code login ON THIS PC — chat and agent run via your subscription'],
267
+ ['ts ia status · ts ia usar chatgpt|claude', 'show/choose your account AI on this PC (automatico goes back to your key)'],
268
+ ['ts conectar nuvem', 'turn off your own key (back to the account AI: your subscription or any remaining legacy credits)'],
240
269
  ] },
241
270
  { title: 'Cloud agents (orchestration)', items: [
242
271
  ['ts run "goal"', 'plan → you approve → execute'],
@@ -246,7 +275,7 @@ const STR = {
246
275
  ] },
247
276
  { title: 'Account', items: [
248
277
  ['ts login · ts logout', 'connect / disconnect this terminal'],
249
- ['ts uso', 'monthly credits and usage'],
278
+ ['ts uso', 'plan, AI in use and legacy credits (if any)'],
250
279
  ['ts quem', 'connected account'],
251
280
  ['ts account export', 'download your data without passwords, tokens or private keys'],
252
281
  ['ts account open', 'open Account for privacy controls and safe deletion'],
@@ -258,7 +287,7 @@ const STR = {
258
287
  ['ts idioma pt|en', 'language (default pt-BR)'],
259
288
  ] },
260
289
  ],
261
- help_footer: 'Every use debits your Smart Credits — track it with `ts uso`.',
290
+ help_footer: 'AI runs via your key (`ts conectar`), via your subscription (`ts ia conectar`) or via local Ollama — see which one is in use with `ts uso`.',
262
291
  repl_hello: 'chat naturally — I detect question, action or big goal · /help · exit',
263
292
  onboard_title: 'First time? Just talk — no commands needed.',
264
293
  onboard_lines: [
@@ -270,7 +299,7 @@ const STR = {
270
299
  route_agent: 'this needs actions on this machine — acting as agent (prefix /chat to just talk)',
271
300
  route_run: 'big goal — planning with the agent team (you approve before spending)',
272
301
  repl_account: (u, p) => `account ${u} · plan ${p}`,
273
- repl_credits: (c) => `${c} credits`,
302
+ repl_credits: (c) => `${c} legacy credits`,
274
303
  repl_help: [
275
304
  ['type anything', 'natural language chat'],
276
305
  ['/agente "task"', 'actually does it on this machine (commands, files)'],
@@ -281,7 +310,7 @@ const STR = {
281
310
  ['/custo', 'what it is costing (and whose bill it is)'],
282
311
  ['/tema [name]', 'switch the terminal palette'],
283
312
  ['/limpar', 'clear the screen (context stays)'],
284
- ['/uso', 'monthly credits'],
313
+ ['/uso', 'plan and AI in use'],
285
314
  ['/nova', 'reset context'],
286
315
  ['exit', 'quit'],
287
316
  ],
@@ -314,11 +343,17 @@ const STR = {
314
343
  run_paused: (r) => `Paused (${r}). Resume on the web or app.`,
315
344
  runs_title: 'Recent orchestrations',
316
345
  runs_empty: 'No orchestrations yet. Start with: ts run "goal"',
317
- uso_title: 'Monthly usage',
346
+ uso_title: 'Usage and account AI',
318
347
  uso_plan: 'Plan',
348
+ uso_ai: 'AI in use',
349
+ uso_ai_none: 'no AI of your own connected — use `ts conectar <provider>` or `ts ia conectar chatgpt|claude`',
350
+ uso_ai_unknown: 'could not confirm the account AI right now (no answer from the server) — check later with `ts uso` or `ts ia status`',
351
+ uso_ai_sub_disconnected: (label, prov) => `${label} mode selected, but the account CLI is not ready on this PC — chat and agent will not run like this; log in with \`ts ia conectar ${prov}\` or switch back with \`ts ia usar automatico\``,
352
+ uso_ai_sub_not_selected: (prov) => `account logged in on this PC but not selected — use \`ts ia usar ${prov}\``,
353
+ byok_no_balance: (prov) => `Your own ${prov} key was refused for balance or limits at the provider. Nothing was charged by Terminal Smart. Add balance at the provider, switch providers with \`ts conectar <provider>\` (or local Ollama) or use your subscription with \`ts ia conectar chatgpt|claude\`.`,
319
354
  uso_remaining: 'remaining',
320
355
  uso_unlimited: 'unlimited',
321
- uso_credits: 'credits',
356
+ uso_credits: 'legacy credits',
322
357
  uso_last: 'Recent usage',
323
358
  quem_title: 'Connected account',
324
359
  quem_user: 'User',
@@ -329,18 +364,21 @@ const STR = {
329
364
  lang_invalid: 'Unsupported language. Use: ts idioma pt or ts idioma en',
330
365
  err_conn: 'Could not reach the server. Check your connection and retry.',
331
366
  err_generic: 'Something went wrong',
332
- no_credits: 'Your AI limit is over. Free renews daily; paid plans renew each cycle. Continue now at terminalsmart.com.br/planos',
367
+ no_credits: 'Terminal Smart AI is not available for this run (no legacy credits). Use your own: `ts conectar <provider>` (your key or local Ollama) or `ts ia conectar chatgpt|claude` (your subscription).',
333
368
  ask_empty: 'Tell me what you need. E.g.: ts "how do I open port 443 on ufw?"',
334
369
  chat_hint: 'type your message · "exit" quits · "/new" resets context',
335
370
  chat_prompt: 'You › ',
336
- chat_bye: 'See you! The conversation stays saved on the web (/ia).',
371
+ chat_bye: 'See you! This folder\'s history stays only on this PC (`ts nova` clears it).',
337
372
  agent_need: 'Describe the task. E.g.: ts agente "free disk space by analyzing the largest folders"',
338
373
  agent_thinking: 'agent thinking',
339
374
  agent_blocked: 'blocked by harness',
340
375
  agent_approve: (cmd) => `The agent wants to run a DESTRUCTIVE command:\n\n ${cmd}\n\n Allow? (y/N) `,
341
- agent_footer: (st, cr, s) => `${st} step(s) · ${cr} credits · ${s}`,
342
- agent_footer_byok: (st, prov, s) => `${st} step(s) · your key (${prov}) · ${s}`,
343
- agent_remote_wait: (ttl) => `destructive command — approval request sent to your Telegram (answer within ${Math.round(ttl / 60)} min; no answer = denied)`,
376
+ agent_footer: (st, cr, s) => `${st} step(s) · ${cr} legacy credits · ${s}`,
377
+ agent_footer_byok: (st, prov, s) => `${st} step(s) · via your key (${prov}) · ${s}`,
378
+ agent_footer_via: (st, via, s) => `${st} step(s) · ${via} · ${s}`,
379
+ via_key: (prov) => `via your key (${prov})`,
380
+ via_subscription: 'via your subscription',
381
+ via_ollama: 'via local Ollama',
344
382
  meta_need: 'Describe the goal. E.g.: ts meta "build a complete portfolio site in this folder" --budget 300',
345
383
  meta_title: 'MISSION',
346
384
  meta_exists: (g) => `A mission is already in progress in this directory ("${g.slice(0, 60)}…"). Run "ts meta" without a goal to RESUME, or use --novo to start over.`,
@@ -0,0 +1,388 @@
1
+ // lib/ia-local.js — a IA da CONTA do usuário rodando NO PC dele.
2
+ //
3
+ // "Conta" aqui é a CLI oficial já logada nesta máquina: Claude Code (`claude`) ou Codex
4
+ // (`codex`, a CLI do ChatGPT). Nada passa pelo servidor do Terminal Smart: o prompt vai
5
+ // pelo stdin do processo local e a resposta volta pelo stdout.
6
+ //
7
+ // Execução só-texto e sem escrita, nas duas CLIs:
8
+ // - pasta temporária VAZIA como cwd (apagada no fim) — nada do projeto fica ao alcance;
9
+ // - prompt pelo stdin, nunca no argv (não aparece na lista de processos nem passa por shell);
10
+ // - execFile/spawn com ARRAY de argumentos, sem shell; no Windows o atalho `.cmd` do npm é
11
+ // lido e o script JS dele roda direto no Node (atalho desconhecido = recusa, sem shell);
12
+ // - ambiente mínimo (só o que a CLI precisa para achar a conta e a rede), sem token do TS
13
+ // nem chaves de API do usuário;
14
+ // - tempo máximo com encerramento da árvore de processos;
15
+ // - Claude: sem ferramentas (`--tools ""`), sem MCP, sem settings/hooks do usuário, sem
16
+ // sessão salva; Codex: sandbox só-leitura, efêmero, sem config/rules do usuário e com as
17
+ // ferramentas de shell/navegador/apps desligadas.
18
+ 'use strict';
19
+ const fs = require('fs');
20
+ const os = require('os');
21
+ const path = require('path');
22
+ const childProcess = require('child_process');
23
+
24
+ const MODOS = ['automatico', 'chatgpt', 'claude', 'paralelo'];
25
+ const PROVEDORES = {
26
+ chatgpt: { bin: 'codex', nome: 'ChatGPT/Codex', envBin: 'TS_CODEX_BIN' },
27
+ claude: { bin: 'claude', nome: 'Claude', envBin: 'TS_CLAUDE_BIN' },
28
+ };
29
+ const PROMPT_MAX = 200000;
30
+ const PRAZO_PADRAO = 180000;
31
+ const PRAZO_STATUS = 8000;
32
+ const SAIDA_MAX = 8 * 1024 * 1024;
33
+
34
+ // Funcionalidades do Codex que dariam ferramentas ao modelo. A sandbox só-leitura já impede
35
+ // escrita; desligar estas tira também a execução de comandos, o navegador e os apps.
36
+ const CODEX_SEM_FERRAMENTAS = ['shell_tool', 'unified_exec', 'apps', 'plugins', 'browser_use',
37
+ 'computer_use', 'in_app_browser', 'multi_agent', 'image_generation'];
38
+
39
+ // Variáveis que a CLI realmente precisa (achar a casa/conta, o sistema e a rede). O resto do
40
+ // ambiente — token do TS, chaves de API, segredos de outros programas — fica de fora.
41
+ const AMBIENTE_PERMITIDO = new Set([
42
+ 'PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'SYSTEMDRIVE', 'OS',
43
+ 'PROCESSOR_ARCHITECTURE', 'NUMBER_OF_PROCESSORS', 'COMPUTERNAME',
44
+ 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA',
45
+ 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'COMMONPROGRAMFILES',
46
+ 'USER', 'USERNAME', 'USERDOMAIN', 'LOGNAME', 'SHELL',
47
+ 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ',
48
+ 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'XDG_RUNTIME_DIR',
49
+ 'DBUS_SESSION_BUS_ADDRESS',
50
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY',
51
+ 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
52
+ 'CODEX_HOME', 'CLAUDE_CONFIG_DIR', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CODE_GIT_BASH_PATH',
53
+ ]);
54
+
55
+ function nomeDe(prov) { return (PROVEDORES[prov] || {}).nome || String(prov); }
56
+ function modoValido(modo) { return MODOS.includes(String(modo || '').toLowerCase()); }
57
+ function rotuloModo(modo) {
58
+ return modo === 'chatgpt' ? 'ChatGPT/Codex' : modo === 'claude' ? 'Claude' : modo === 'paralelo' ? 'ChatGPT/Codex + Claude' : 'automático';
59
+ }
60
+ function comoLogar(prov) {
61
+ return prov === 'chatgpt'
62
+ ? 'Rode `ts ia conectar chatgpt` (abre o `codex login` oficial).'
63
+ : 'Rode `claude` e use /login (ou `ts ia conectar claude` para ver o passo a passo).';
64
+ }
65
+ function comoInstalar(prov) {
66
+ return prov === 'chatgpt'
67
+ ? 'Instale a CLI oficial do Codex (`npm install -g @openai/codex`) e faça o login com `codex login`.'
68
+ : 'Instale o Claude Code oficial (https://claude.com/claude-code) e faça o login com `claude` → /login.';
69
+ }
70
+
71
+ function erroIa(codigo, mensagem) { const e = new Error(mensagem); e.code = codigo; return e; }
72
+
73
+ function ambienteMinimo(base = process.env) {
74
+ const out = {};
75
+ for (const [k, v] of Object.entries(base || {})) {
76
+ if (v != null && AMBIENTE_PERMITIDO.has(k.toUpperCase())) out[k] = String(v);
77
+ }
78
+ out.NO_COLOR = '1';
79
+ out.FORCE_COLOR = '0';
80
+ return out;
81
+ }
82
+
83
+ // Atalho `.cmd` que o npm gera no Windows: a última linha chama `node "%dp0%\...\x.js" %*`.
84
+ // Lendo o caminho do script, rodamos `node script.js args` direto — sem cmd.exe no meio.
85
+ function scriptDoAtalhoNpm(arquivo) {
86
+ let texto = '';
87
+ try { texto = fs.readFileSync(arquivo, 'utf8'); } catch (_) { return null; }
88
+ const m = texto.match(/"%~?dp0%?\\([^"%]+\.(?:c|m)?js)"/i);
89
+ if (!m) return null;
90
+ const script = path.resolve(path.dirname(arquivo), m[1].replace(/\\/g, path.sep));
91
+ return fs.existsSync(script) ? script : null;
92
+ }
93
+
94
+ // Como rodar um arquivo executável SEM shell. Retorna { cmd, prefixo, caminho } ou { erro }.
95
+ function comoExecutar(arquivo, plataforma = process.platform) {
96
+ const ext = path.extname(arquivo).toLowerCase();
97
+ if (['.js', '.cjs', '.mjs'].includes(ext)) return { cmd: process.execPath, prefixo: [arquivo], caminho: arquivo };
98
+ if (plataforma === 'win32' && ['.cmd', '.bat'].includes(ext)) {
99
+ const script = scriptDoAtalhoNpm(arquivo);
100
+ if (script) return { cmd: process.execPath, prefixo: [script], caminho: arquivo };
101
+ return { caminho: arquivo, erro: `não dá para executar com segurança o atalho "${path.basename(arquivo)}" sem passar pelo cmd.exe. Aponte TS_CODEX_BIN/TS_CLAUDE_BIN para o executável (.exe) ou para o script .js da CLI.` };
102
+ }
103
+ return { cmd: arquivo, prefixo: [], caminho: arquivo };
104
+ }
105
+
106
+ function criar(opcoes = {}) {
107
+ const {
108
+ bins = {},
109
+ spawnImpl = childProcess.spawn,
110
+ env = process.env,
111
+ homedir = os.homedir(),
112
+ tmpdir = os.tmpdir(),
113
+ plataforma = process.platform,
114
+ } = opcoes;
115
+
116
+ function ehArquivo(p) { try { return fs.statSync(p).isFile(); } catch (_) { return false; } }
117
+ function executavelPosix(p) { try { fs.accessSync(p, fs.constants.X_OK); return true; } catch (_) { return false; } }
118
+
119
+ function procurarNoPath(nome) {
120
+ const sep = plataforma === 'win32' ? ';' : ':';
121
+ const dirs = String(env.PATH || env.Path || '').split(sep).map(d => d.trim().replace(/^"|"$/g, '')).filter(Boolean);
122
+ // instalações oficiais que nem sempre estão no PATH do terminal atual
123
+ dirs.push(path.join(homedir, '.local', 'bin'), path.join(homedir, '.claude', 'local'));
124
+ if (plataforma === 'win32' && env.APPDATA) dirs.push(path.join(env.APPDATA, 'npm'));
125
+ if (plataforma !== 'win32') dirs.push('/usr/local/bin', '/opt/homebrew/bin');
126
+ // no Windows o atalho sem extensão do npm é um script sh: só .exe/.cmd/.bat servem
127
+ const exts = plataforma === 'win32' ? ['.exe', '.cmd', '.bat'] : [''];
128
+ for (const ext of exts) {
129
+ for (const d of dirs) {
130
+ const p = path.join(d, nome + ext);
131
+ if (ehArquivo(p) && (plataforma === 'win32' || executavelPosix(p))) return p;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function resolverExecutavel(prov) {
138
+ const info = PROVEDORES[prov];
139
+ if (!info) return null;
140
+ const custom = bins[prov] || env[info.envBin];
141
+ const alvo = custom ? path.resolve(String(custom)) : procurarNoPath(info.bin);
142
+ if (!alvo || !ehArquivo(alvo)) return null;
143
+ return comoExecutar(alvo, plataforma);
144
+ }
145
+
146
+ function arquivoCredencial(prov) {
147
+ if (prov === 'chatgpt') return path.join(env.CODEX_HOME || path.join(homedir, '.codex'), 'auth.json');
148
+ return path.join(env.CLAUDE_CONFIG_DIR || path.join(homedir, '.claude'), '.credentials.json');
149
+ }
150
+
151
+ function encerrar(proc) {
152
+ if (!proc) return;
153
+ if (plataforma === 'win32' && proc.pid) {
154
+ // o script do npm abre o binário nativo como filho: mata a árvore inteira
155
+ try { childProcess.execFile('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { windowsHide: true }, () => {}); } catch (_) {}
156
+ }
157
+ try { proc.kill('SIGKILL'); } catch (_) {}
158
+ }
159
+
160
+ // Roda a CLI e junta a saída. stdin = texto (fechado logo depois) ou null (fechado vazio).
161
+ function rodar(exe, args, { stdin = null, cwd, timeoutMs = PRAZO_PADRAO } = {}) {
162
+ return new Promise((resolve, reject) => {
163
+ let proc;
164
+ try {
165
+ proc = spawnImpl(exe.cmd, [...exe.prefixo, ...args], {
166
+ cwd, env: ambienteMinimo(env), stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, shell: false,
167
+ });
168
+ } catch (e) { reject(erroIa('falha_execucao', `não consegui iniciar ${path.basename(exe.caminho)}: ${e.message}`)); return; }
169
+ let stdout = '', stderr = '', fim = false, tamanho = 0;
170
+ const timer = setTimeout(() => {
171
+ if (fim) return; fim = true; encerrar(proc);
172
+ reject(erroIa('timeout', `a IA local não respondeu em ${Math.round(timeoutMs / 1000)} s e foi encerrada.`));
173
+ }, timeoutMs);
174
+ if (timer.unref) timer.unref();
175
+ const juntar = (alvo) => (chunk) => {
176
+ tamanho += chunk.length;
177
+ if (tamanho > SAIDA_MAX) { if (!fim) { fim = true; clearTimeout(timer); encerrar(proc); reject(erroIa('saida_grande', 'a IA local devolveu uma saída grande demais e foi encerrada.')); } return; }
178
+ if (alvo === 'out') stdout += chunk; else stderr += chunk;
179
+ };
180
+ if (proc.stdout) { proc.stdout.setEncoding('utf8'); proc.stdout.on('data', juntar('out')); }
181
+ if (proc.stderr) { proc.stderr.setEncoding('utf8'); proc.stderr.on('data', juntar('err')); }
182
+ proc.once('error', (e) => {
183
+ if (fim) return; fim = true; clearTimeout(timer);
184
+ reject(erroIa(e && e.code === 'ENOENT' ? 'nao_instalada' : 'falha_execucao', `não consegui iniciar ${path.basename(exe.caminho)}: ${e.message}`));
185
+ });
186
+ proc.once('close', (code) => {
187
+ if (fim) return; fim = true; clearTimeout(timer);
188
+ resolve({ code, stdout, stderr });
189
+ });
190
+ if (proc.stdin) {
191
+ proc.stdin.on('error', () => {}); // EPIPE quando a CLI fecha antes de ler tudo
192
+ proc.stdin.end(stdin == null ? '' : String(stdin), 'utf8');
193
+ }
194
+ });
195
+ }
196
+
197
+ async function statusDe(prov, { rapido = false, prazoMs = PRAZO_STATUS } = {}) {
198
+ const exe = resolverExecutavel(prov);
199
+ if (!exe) return { instalada: false, logada: false, caminho: null };
200
+ if (exe.erro) return { instalada: true, logada: null, caminho: exe.caminho, detalhe: exe.erro };
201
+ if (ehArquivo(arquivoCredencial(prov))) return { instalada: true, logada: true, caminho: exe.caminho };
202
+ if (rapido) return { instalada: true, logada: null, caminho: exe.caminho };
203
+ // Sem o arquivo (ex.: macOS guarda no Keychain): pergunta à própria CLI, com prazo curto.
204
+ const args = prov === 'chatgpt' ? ['login', 'status'] : ['auth', 'status', '--json'];
205
+ let pasta = null;
206
+ try {
207
+ pasta = fs.mkdtempSync(path.join(tmpdir, 'ts-ia-local-'));
208
+ const r = await rodar(exe, args, { cwd: pasta, timeoutMs: prazoMs });
209
+ const saida = (r.stdout + '\n' + r.stderr).trim();
210
+ let logada;
211
+ if (prov === 'claude') {
212
+ let j = null; try { j = JSON.parse(r.stdout); } catch (_) {}
213
+ logada = j && typeof j.loggedIn === 'boolean' ? j.loggedIn : (r.code === 0 && !/not logged in|não está logad/i.test(saida));
214
+ } else {
215
+ logada = r.code === 0 && !/not logged in|não está logad/i.test(saida);
216
+ }
217
+ return { instalada: true, logada, caminho: exe.caminho };
218
+ } catch (e) {
219
+ return { instalada: true, logada: null, caminho: exe.caminho, detalhe: e.message };
220
+ } finally {
221
+ if (pasta) try { fs.rmSync(pasta, { recursive: true, force: true }); } catch (_) {}
222
+ }
223
+ }
224
+
225
+ // { chatgpt:{instalada, logada, caminho}, claude:{...} } — logada null = não deu para confirmar.
226
+ async function status(opts = {}) {
227
+ const [chatgpt, claude] = await Promise.all([statusDe('chatgpt', opts), statusDe('claude', opts)]);
228
+ return { chatgpt, claude };
229
+ }
230
+
231
+ // A CLI exigida pelo modo está pronta? Login "não confirmado" (null) conta como pronta:
232
+ // a execução real dá o erro claro, se for o caso.
233
+ function pronto(modo, st) {
234
+ const ok = (p) => !!(st && st[p] && st[p].instalada && st[p].logada !== false && !st[p].detalhe);
235
+ if (modo === 'chatgpt' || modo === 'claude') return ok(modo);
236
+ if (modo === 'paralelo') return ok('chatgpt') && ok('claude');
237
+ return false;
238
+ }
239
+ // Qual CLI falta para o modo (texto para o usuário).
240
+ function faltando(modo, st) {
241
+ const precisa = modo === 'paralelo' ? ['chatgpt', 'claude'] : [modo];
242
+ const faltam = precisa.filter(p => !pronto(p, st));
243
+ return faltam.map(p => {
244
+ const s = (st && st[p]) || {};
245
+ return `${nomeDe(p)} ${!s.instalada ? 'não está instalado neste PC' : s.detalhe ? `não pode ser executado (${s.detalhe})` : 'não está logado neste PC'}`;
246
+ }).join(' e ');
247
+ }
248
+
249
+ function argsCodex() {
250
+ return ['exec', '--json', '--ephemeral', '--ignore-user-config', '--ignore-rules',
251
+ '--sandbox', 'read-only', '--skip-git-repo-check', '--color', 'never',
252
+ ...CODEX_SEM_FERRAMENTAS.flatMap(f => ['--disable', f]),
253
+ '-'];
254
+ }
255
+ function argsClaude(sistema) {
256
+ return ['-p', '--output-format', 'json', '--tools', '', '--permission-mode', 'dontAsk',
257
+ '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}', '--setting-sources', '',
258
+ '--no-session-persistence', '--disable-slash-commands',
259
+ ...(sistema ? ['--system-prompt', String(sistema)] : [])];
260
+ }
261
+
262
+ function erroClaro(prov, detalheBruto) {
263
+ const nome = nomeDe(prov);
264
+ const detalhe = String(detalheBruto || '').replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').trim();
265
+ if (prov === 'chatgpt' && /token_revoked|refresh token (?:was )?(?:revoked|invalidated)|session has ended|log out and sign in again/i.test(detalhe)) {
266
+ return erroIa('sessao_expirada', `A sessão do ${nome} expirou ou foi revogada neste PC. Rode \`codex login\` (ou \`ts ia conectar chatgpt\`) para entrar de novo.`);
267
+ }
268
+ if (/not logged in|please (?:run )?[`'"]?(?:\/?login|codex login|claude login)|login required|invalid api key|authentication[_ ]error|unauthorized|\b401\b|no credentials|oauth token (?:has )?expired/i.test(detalhe)) {
269
+ return erroIa('nao_logada', `${nome} não está logado neste PC. ${comoLogar(prov)}`);
270
+ }
271
+ if (/unknown option|unexpected argument|unrecognized (?:option|argument)|error: unknown/i.test(detalhe)) {
272
+ return erroIa('desatualizada', `A versão instalada do ${nome} não aceita as opções seguras que o Terminal Smart usa. Atualize com \`${prov === 'chatgpt' ? 'codex update' : 'claude update'}\` e tente de novo.`);
273
+ }
274
+ if (/usage limit|rate limit|quota|\b429\b|limit reached/i.test(detalhe)) {
275
+ return erroIa('limite', `O plano da sua conta ${nome} atingiu o limite de uso agora. Espere a janela do plano renovar ou use outra IA (\`ts ia usar ...\` ou \`ts conectar <provedor>\`).`);
276
+ }
277
+ let resumo = detalhe.split(/\r?\n/).filter(Boolean).slice(-4).join(' ').slice(-400);
278
+ try { resumo = require('./core').redactSecrets(resumo); } catch (_) {}
279
+ return erroIa('falha_ia_local', `${nome} falhou neste PC${resumo ? ': ' + resumo : ' sem mensagem.'}`);
280
+ }
281
+
282
+ function interpretar(prov, r, inicio) {
283
+ const uso = { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 };
284
+ let texto = '', modelo = 'padrão da sua conta';
285
+ if (prov === 'chatgpt') {
286
+ const eventos = String(r.stdout || '').split(/\r?\n/).map(l => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
287
+ const falha = eventos.find(e => e.type === 'error' || e.type === 'turn.failed');
288
+ const msgs = eventos.filter(e => e.type === 'item.completed' && e.item && e.item.type === 'agent_message').map(e => e.item.text).filter(Boolean);
289
+ const fimTurno = [...eventos].reverse().find(e => e.type === 'turn.completed');
290
+ if (fimTurno && fimTurno.usage) {
291
+ uso.input_tokens = Number(fimTurno.usage.input_tokens || 0);
292
+ uso.cached_input_tokens = Number(fimTurno.usage.cached_input_tokens || 0);
293
+ uso.output_tokens = Number(fimTurno.usage.output_tokens || 0);
294
+ }
295
+ texto = msgs.join('\n\n').trim();
296
+ if (!texto && (falha || r.code !== 0)) {
297
+ const det = falha ? (falha.message || (falha.error && falha.error.message) || JSON.stringify(falha)) : '';
298
+ throw erroClaro(prov, [det, r.stderr, r.stdout].filter(Boolean).join('\n'));
299
+ }
300
+ } else {
301
+ let j = null;
302
+ try { j = JSON.parse(String(r.stdout || '').trim()); } catch (_) {}
303
+ if (!j) {
304
+ if (r.code !== 0) throw erroClaro(prov, r.stderr || r.stdout);
305
+ texto = String(r.stdout || '').trim();
306
+ } else {
307
+ if (j.is_error || (j.subtype && j.subtype !== 'success')) throw erroClaro(prov, [j.result, j.error, r.stderr].filter(Boolean).join('\n') || j.subtype);
308
+ texto = String(j.result || '').trim();
309
+ modelo = Object.keys(j.modelUsage || {})[0] || j.model || modelo;
310
+ if (j.usage) {
311
+ uso.input_tokens = Number(j.usage.input_tokens || 0);
312
+ uso.cached_input_tokens = Number(j.usage.cache_read_input_tokens || 0);
313
+ uso.output_tokens = Number(j.usage.output_tokens || 0);
314
+ }
315
+ }
316
+ }
317
+ if (!texto) throw erroIa('sem_resposta', `${nomeDe(prov)} encerrou sem resposta.`);
318
+ return { provedor: prov, texto, uso, modelo, duracaoMs: Math.max(0, Date.now() - inicio) };
319
+ }
320
+
321
+ // Executa UM prompt na CLI do provedor. sistema: instrução de sistema (Claude recebe como
322
+ // --system-prompt; no Codex vai no começo do prompt). Devolve { texto, uso, modelo, provedor }.
323
+ async function executar(prov, prompt, { sistema = null, timeoutMs = PRAZO_PADRAO } = {}) {
324
+ if (!PROVEDORES[prov]) throw erroIa('provedor_invalido', 'Provedor de IA local inválido: use chatgpt ou claude.');
325
+ const exe = resolverExecutavel(prov);
326
+ if (!exe) throw erroIa('nao_instalada', `${nomeDe(prov)} não está instalado neste PC. ${comoInstalar(prov)}`);
327
+ if (exe.erro) throw erroIa('execucao_insegura', `${nomeDe(prov)}: ${exe.erro}`);
328
+ let texto = String(prompt || '');
329
+ if (prov === 'chatgpt' && sistema) texto = String(sistema) + '\n\n---\n\n' + texto;
330
+ texto = texto.slice(0, PROMPT_MAX);
331
+ const prazo = Math.max(5000, Number(timeoutMs) || PRAZO_PADRAO);
332
+ const inicio = Date.now();
333
+ const pasta = fs.mkdtempSync(path.join(tmpdir, 'ts-ia-local-'));
334
+ try {
335
+ let r = await rodar(exe, prov === 'chatgpt' ? argsCodex() : argsClaude(sistema), { stdin: texto, cwd: pasta, timeoutMs: prazo });
336
+ // Versão do Codex que não conhece alguma das funcionalidades desligadas: NÃO repete sem
337
+ // elas (seria rodar com menos travas sem o usuário saber); pede para atualizar.
338
+ if (prov === 'chatgpt' && r.code !== 0 && /feature/i.test(r.stderr) && /unknown|invalid|unrecognized|not (?:a )?(?:known|valid)/i.test(r.stderr)) {
339
+ throw erroIa('desatualizada', `A versão instalada do ${nomeDe(prov)} não aceita as opções seguras que o Terminal Smart usa. Atualize o Codex e tente de novo.`);
340
+ }
341
+ return interpretar(prov, r, inicio);
342
+ } finally {
343
+ try { fs.rmSync(pasta, { recursive: true, force: true }); } catch (_) {}
344
+ }
345
+ }
346
+
347
+ // Modo da conta → { modo, resultados:[...], parcial }. paralelo roda as duas ao mesmo tempo.
348
+ async function responder(modo, prompt, opts = {}) {
349
+ if (modo === 'chatgpt' || modo === 'claude') return { modo, resultados: [await executar(modo, prompt, opts)], parcial: false };
350
+ if (modo === 'paralelo') {
351
+ const settled = await Promise.allSettled([executar('chatgpt', prompt, opts), executar('claude', prompt, opts)]);
352
+ const resultados = settled.filter(s => s.status === 'fulfilled').map(s => s.value);
353
+ if (!resultados.length) {
354
+ const motivos = settled.map(s => s.reason && s.reason.message).filter(Boolean).join(' | ');
355
+ throw erroIa('sem_resposta', 'Nenhuma das IAs da sua conta respondeu neste PC' + (motivos ? ': ' + motivos : '.'));
356
+ }
357
+ return { modo, resultados, parcial: resultados.length !== 2 };
358
+ }
359
+ throw erroIa('modo_invalido', 'Modo de IA local inválido: use chatgpt, claude ou paralelo.');
360
+ }
361
+
362
+ // Login OFICIAL no PC, com o terminal do usuário (stdio herdado). Só o Codex tem um comando
363
+ // de login direto e estável; o Claude é orientado a usar /login dentro do `claude`.
364
+ function abrirLogin(prov) {
365
+ return new Promise((resolve, reject) => {
366
+ const exe = resolverExecutavel(prov);
367
+ if (!exe) { reject(erroIa('nao_instalada', `${nomeDe(prov)} não está instalado neste PC. ${comoInstalar(prov)}`)); return; }
368
+ if (exe.erro) { reject(erroIa('execucao_insegura', `${nomeDe(prov)}: ${exe.erro}`)); return; }
369
+ if (prov !== 'chatgpt') { reject(erroIa('sem_login_direto', comoLogar(prov))); return; }
370
+ let proc;
371
+ try { proc = spawnImpl(exe.cmd, [...exe.prefixo, 'login'], { stdio: 'inherit', env, shell: false }); }
372
+ catch (e) { reject(e); return; }
373
+ proc.once('error', reject);
374
+ proc.once('close', code => resolve(code));
375
+ });
376
+ }
377
+
378
+ return {
379
+ status, statusDe, pronto, faltando, executar, responder, abrirLogin, resolverExecutavel,
380
+ _test: { argsCodex, argsClaude, erroClaro, interpretar, arquivoCredencial, procurarNoPath, rodar },
381
+ };
382
+ }
383
+
384
+ const padrao = criar();
385
+ module.exports = Object.assign({}, padrao, {
386
+ criar, MODOS, PROVEDORES, nomeDe, modoValido, rotuloModo, comoLogar, comoInstalar,
387
+ _util: { ambienteMinimo, comoExecutar, scriptDoAtalhoNpm, CODEX_SEM_FERRAMENTAS },
388
+ });