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/README.md +7 -5
- package/bin/ts.js +344 -196
- package/hub/cli.js +255 -0
- package/hub/controle.js +262 -0
- package/hub/dpapi.js +55 -0
- package/hub/estado.js +274 -0
- package/hub/exportar-telas.js +149 -0
- package/hub/hub.js +717 -0
- package/hub/mascara.js +67 -0
- package/hub/parar.js +151 -0
- package/hub/principal.js +5 -0
- package/hub/proto/PROTOCOLO.md +789 -0
- package/hub/proto/ts-proto.js +1643 -0
- package/hub/relay-conexao.js +200 -0
- package/hub/telas-arquivo.js +130 -0
- package/hub/tmux.js +189 -0
- package/hub/vps/README.md +109 -0
- package/hub/vps/tmux-agentes.conf +5 -0
- package/hub/vps/ts-hub-exportar-telas.service +30 -0
- package/hub/vps/ts-hub-parar-consumir +67 -0
- package/hub/vps/ts-hub-parar.path +16 -0
- package/hub/vps/ts-hub-parar.service +23 -0
- package/hub/vps/ts-hub-parar.tmpfiles.conf +10 -0
- package/hub/vps/ts-hub.service +39 -0
- package/hub/vps/ts-tmux.service +19 -0
- package/lib/acp.js +8 -2
- package/lib/agent.js +186 -87
- package/lib/chat-local.js +139 -0
- package/lib/core.js +336 -23
- package/lib/doctor.js +13 -5
- package/lib/i18n.js +69 -31
- package/lib/ia-local.js +388 -0
- package/lib/intelligence-core.js +108 -1
- package/lib/keyring.js +0 -1
- package/lib/meta.js +8 -8
- package/lib/personal-agent-llm.js +99 -26
- package/lib/sentinela.js +2 -2
- package/lib/tools.js +638 -8
- package/package.json +4 -2
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 (
|
|
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
|
|
47
|
-
['ts sentinela instalar', 'agenda os checks no SO (cron/Task Scheduler)
|
|
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', '
|
|
67
|
-
['ts conectar
|
|
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', '
|
|
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: '
|
|
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', '
|
|
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
|
|
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: '
|
|
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!
|
|
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
|
-
|
|
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 (
|
|
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)
|
|
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', '
|
|
239
|
-
['ts conectar
|
|
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', '
|
|
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: '
|
|
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', '
|
|
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: '
|
|
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: '
|
|
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!
|
|
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
|
-
|
|
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.`,
|
package/lib/ia-local.js
ADDED
|
@@ -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
|
+
});
|