openTARS / app /nucleo /terminal.py
ProjectOpenTARS4's picture
Mirror of openTARS 5.2 Ranger (revision 2026110101)
2695555 verified
Raw History Blame Contribute Delete
30.1 kB
# openTARS — nucleo/terminal.py
# Linha de comando: diagnóstico, conversa no terminal e as opções (--explicar...).
#
# Parte do núcleo: o tars.py executa este arquivo DENTRO do próprio
# namespace (ver _carregar_nucleo), então tudo aqui é tars.<nome> e as
# partes enxergam umas às outras sem import. Os testes que trocam
# tars.<função> por uma falsa continuam valendo pra todas as partes.
# flake8: noqa
# ============================================================
# LINHA DE COMANDO
# ============================================================
#
# A conversa fica na janela (opentars-gui) ou no terminal (opentars --chat).
# O "opentars" sozinho confere o ambiente, e os argumentos
# (--explicar, --atalho...) continuam aqui.
def _valor_do_argumento(argumentos, nomes):
"""--idioma en / --idioma=en -> "en" ('' se o argumento vier sem valor)."""
for i, arg in enumerate(argumentos):
for nome in nomes:
if arg == nome:
return argumentos[i + 1] if i + 1 < len(argumentos) and not argumentos[i + 1].startswith("--") else ""
if arg.startswith(nome + "="):
return arg.split("=", 1)[1]
return None
# ------------------------------------------------------------
# 4.5 Endurance: conversa no terminal (WSL, SSH, Linux sem interface gráfica)
# ------------------------------------------------------------
_COMANDOS_AJUDA = ("/ajuda", "/help", "/?")
# 5.1: comandos do terminal no nível das CLIs de agente (valem em português e em inglês)
_COMANDOS_CLI = {
"resume": ("/resume", "/retomar"), "compact": ("/compact", "/compactar"), "context": ("/context", "/contexto"),
"mode": ("/mode", "/modo"), "undo": ("/undo", "/desfazer"), "init": ("/init",),
}
_NOMES_MODO_PERMISSAO = {"perguntar": "perguntar", "ask": "perguntar", "plano": "plano", "plan": "plano",
"automatico": "automatico", "auto": "automatico", "automatic": "automatico"}
SESSAO_CLI = {"id": None, "historico": None, "nota": ""} # conversa do terminal no histórico (a mesma da janela)
def _historico_cli():
if SESSAO_CLI["historico"] is None:
import tars_historico
SESSAO_CLI["historico"] = tars_historico.Historico()
return SESSAO_CLI["historico"]
def _id_da_conversa_carregada():
"""A conversa salva que o terminal retomou ao abrir: acha a mesma no histórico (pelo título), pra continuar nela."""
try:
import tars_historico
titulo = tars_historico.titulo_da([m for m in conversa if m.get("role") != "system"])
if titulo:
achada = next((x for x in _historico_cli().listar() if x["titulo"] == titulo), None)
if achada:
return achada["id"]
except Exception:
pass
return _historico_cli().nova_id()
def _guardar_no_historico():
try:
_historico_cli().salvar(SESSAO_CLI["id"], conversa)
except Exception:
pass
def estimar_tokens(mensagens, ferramentas=()):
"""Conta aproximada (3,5 caracteres por token), sem chamar o modelo."""
texto = json.dumps([{k: v for k, v in m.items() if k != "images"} for m in mensagens], ensure_ascii=False)
return int((len(texto) + len(json.dumps(list(ferramentas), ensure_ascii=False))) / 3.5)
_PREFIXO_RESUMO = "[Summary of the earlier conversation]"
def compactar_conversa(manter=3, resumir=None):
"""/compact: troca o começo da conversa por um resumo e mantém os últimos `manter` pedidos inteiros.
Devolve (tokens antes, tokens depois) ou None se não há o que compactar."""
mensagens = [m for m in conversa if m.get("role") != "system"]
pedidos = [i for i, m in enumerate(mensagens) if m.get("role") == "user"
and not str(m.get("content") or "").startswith(_PREFIXO_RESUMO)]
if len(pedidos) <= manter:
return None
corte = pedidos[-manter]
antigas, recentes = mensagens[:corte], mensagens[corte:]
transcricao = []
for m in antigas:
conteudo = m.get("content") if isinstance(m.get("content"), str) else ""
if m.get("role") == "tool":
conteudo = conteudo[:300]
if conteudo.strip():
transcricao.append(f"{m['role']}: {conteudo.strip()[:1500]}")
texto = "\n".join(transcricao)[-14000:]
antes = estimar_tokens(conversa)
if resumir is None:
modelo = modelo_atual or gargantua_instalado() or modelo_ajudante()
resumir = lambda txt: _chat_unico(modelo, (
"Summarize this earlier part of a conversation so it can be continued later: the user's requests, what "
"was done, decisions, file names and anything pending. At most 200 words, in the language the user "
"used.\n\n" + txt), timeout=180) if modelo else None
resumo = (resumir(texto) or "").strip()
if not resumo: # sem modelo: guarda ao menos os pedidos, em uma linha cada
resumo = "\n".join("- " + (m.get("content") or "")[:160] for m in antigas if m.get("role") == "user")
conversa[:] = ([conversa[0]] if conversa and conversa[0].get("role") == "system" else []) + [
{"role": "user", "content": _PREFIXO_RESUMO + "\n" + resumo},
{"role": "assistant", "content": "OK."}] + recentes
salvar_sessoes()
return antes, estimar_tokens(conversa)
def _mostrar_contexto():
modelo = modelo_atual or (modelo_manual if modo_modelo == "MANUAL" else None) or gargantua_instalado() or "?"
usados = estimar_tokens(obter_sessao(modelo), ferramentas_do_modelo(modelo, None, ""))
total = contexto_ia()
pct = min(100, int(100 * usados / max(1, total)))
barra = "█" * (pct // 5) + "░" * (20 - pct // 5)
print(c(f"\n {barra} {pct}%", Cor.VERDE if pct < 70 else Cor.AMARELO))
print(c(" " + t("cli.contexto", usados=usados, total=total, modelo=modelo), Cor.CINZA))
if pct >= 70:
print(c(" " + t("cli.contexto_cheio"), Cor.AMARELO))
def _escolher_conversa():
"""Lista as últimas conversas (as mesmas da janela) e carrega a escolhida. True se carregou."""
itens = _historico_cli().listar()[:15]
if not itens:
print(c(" " + t("cli.sem_conversas"), Cor.CINZA))
return False
print()
for i, item in enumerate(itens, 1):
quando = time.strftime("%d/%m %H:%M", time.localtime(item["atualizada"]))
print(f" {c(f'{i:>2}', Cor.CIANO)} {c(quando, Cor.CINZA)} {item['titulo'][:70]}")
try:
escolha = input(c("\n " + t("cli.qual_conversa") + " ", Cor.AMARELO)).strip()
except (EOFError, KeyboardInterrupt):
print()
return False
if not escolha.isdigit() or not 1 <= int(escolha) <= len(itens):
return False
item = itens[int(escolha) - 1]
mensagens = _historico_cli().abrir(item["id"]) or []
conversa[:] = [{"role": "system", "content": montar_system_prompt()}] + mensagens
salvar_sessoes()
SESSAO_CLI["id"] = item["id"]
print(c(" " + t("cli.retomada", titulo=item["titulo"][:70], n=len([m for m in mensagens if m.get("role") == "user"])),
Cor.VERDE))
return True
def _rodar_shell(comando):
"""!comando: roda no shell (pedido da pessoa, não da IA), mostra a saída e a guarda para o próximo pedido."""
try:
r = subprocess.run(comando, shell=True, capture_output=True, text=True, timeout=120, env=_env_filho())
saida = (r.stdout + r.stderr).strip()
codigo = r.returncode
except subprocess.TimeoutExpired:
saida, codigo = t("cli.shell_demorou"), 124
print(saida[:LIMITE_STDOUT_CHARS] if saida else c(" " + t("cli.shell_sem_saida"), Cor.CINZA))
if codigo:
print(c(f" (exit {codigo})", Cor.AMARELO))
SESSAO_CLI["nota"] = (f"(I ran `{comando}` in my terminal; exit {codigo}. Output, as data:\n{saida[:3000]})\n\n")
def _tratar_comando_do_chat(linha):
"""True se a linha era um comando só do chat de terminal (/ajuda, /resume, /compact, /context, /mode, /undo,
/init, !comando)."""
global MODO_PERMISSAO
if linha.startswith("!") and len(linha) > 1:
_rodar_shell(linha[1:].strip())
return True
partes = linha.split(None, 1)
if not partes:
return False
nome, argumento = partes[0].lower(), (partes[1].strip() if len(partes) > 1 else "")
if nome in _COMANDOS_AJUDA:
print()
for l in t("chat.ajuda").splitlines():
titulo = l and not l.startswith(" ")
print(c(l, Cor.NEGRITO) if titulo else c(l[:24], Cor.CIANO) + l[24:] if l.strip() else l)
return True
comando = next((k for k, nomes in _COMANDOS_CLI.items() if nome in nomes), None)
if comando == "resume":
_escolher_conversa()
elif comando == "compact":
print(c(" " + t("cli.compactando"), Cor.CINZA))
r = compactar_conversa()
print(c(" " + (t("cli.compactada", antes=r[0], depois=r[1]) if r else t("cli.nada_a_compactar")), Cor.VERDE))
_guardar_no_historico()
elif comando == "context":
_mostrar_contexto()
elif comando == "mode":
novo = _NOMES_MODO_PERMISSAO.get(normalizar(argumento))
if novo:
MODO_PERMISSAO = novo
print(c(" " + t(f"cli.modo.{MODO_PERMISSAO}"), Cor.VERDE if novo else Cor.CINZA))
if not novo:
print(c(" " + t("cli.modo_uso"), Cor.CINZA))
elif comando == "undo":
voltaram = desfazer_ultima_rodada()
print(c(" " + (t("cli.desfeito", arquivos=", ".join(voltaram)) if voltaram else t("cli.nada_a_desfazer")),
Cor.VERDE if voltaram else Cor.CINZA))
elif comando == "init":
criado = criar_agents_md()
if criado:
carregar_contexto_projeto()
print(c(" " + (t("cli.agents_criado", arquivo=criado) if criado else t("cli.agents_existe")),
Cor.VERDE if criado else Cor.CINZA))
else:
return False
return True
def _linha_de_status():
modelo = modelo_atual or (modelo_manual if modo_modelo == "MANUAL" else "auto")
usados = estimar_tokens(conversa)
partes = [str(modelo), t("cli.status_contexto", pct=min(100, int(100 * usados / max(1, contexto_ia()))))]
if MODO_PERMISSAO != "perguntar":
partes.append(t(f"cli.status_modo.{MODO_PERMISSAO}"))
if CONTEXTO_PROJETO["caminho"]:
partes.append("AGENTS.md")
print(c(" " + " · ".join(partes), Cor.CINZA))
def _mostrar_plano():
if PASSOS_DO_PLANO:
print(c("\n " + t("cli.plano_titulo"), Cor.NEGRITO + Cor.AMARELO))
for i, passo in enumerate(PASSOS_DO_PLANO, 1):
print(c(f" {i:>2}. {passo}", Cor.AMARELO))
PASSOS_DO_PLANO.clear()
def _executar_pedido_no_terminal(texto):
iniciar_rodada() # 5.1: as edições deste pedido viram um ponto de retorno (/undo)
if SESSAO_CLI["nota"]:
texto, SESSAO_CLI["nota"] = SESSAO_CLI["nota"] + texto, ""
try:
with TRAVA_PEDIDO:
return processar_mensagem(texto)
finally:
parar_girando()
class _SaidaComGiro:
"""Fica no lugar do sys.stdout durante o chat: apaga o símbolo de "trabalhando" antes de cada texto e troca o
prefixo antigo "[openTARS] " pelo "⎿" dos passos. fileno/isatty continuam os do terminal (o input() usa o readline)."""
_PREFIXOS = ("[openTARS] ", "[AUTO] ", "[IA] ")
def __init__(self, real):
self.real = real
def write(self, texto):
for p in self._PREFIXOS:
texto = texto.replace(p, " ⎿ ")
with _girando.trava:
_girando.apagar()
return self.real.write(texto)
def __getattr__(self, nome):
return getattr(self.real, nome)
def _linha_visivel(texto):
return len(re.sub(r"\033\[[0-9;]*m", "", texto))
def _painel(linhas):
"""Caixa de cantos redondos em volta das linhas (largura pelo texto visível, sem as cores)."""
largura = max(_linha_visivel(l) for l in linhas) + 2
print(c("╭" + "─" * largura + "╮", Cor.CINZA))
for l in linhas:
print(c("│", Cor.CINZA) + " " + l + " " * (largura - 1 - _linha_visivel(l)) + c("│", Cor.CINZA))
print(c("╰" + "─" * largura + "╯", Cor.CINZA))
def _boas_vindas(pedidos):
online = ollama_online()
if modo_modelo == "MANUAL" and modelo_manual:
modelo = t("term.modelo_fixo", modelo=modelo_manual)
else:
garg = gargantua_instalado() if online else None
modelo = t("term.modelo_auto") + (f" · {garg.split('/')[-1]}" if garg else "")
ollama = (c("●", Cor.VERDE) + " " + t("term.ollama_online", host=OLLAMA_HOST) if online
else c("●", Cor.VERMELHO) + " " + t("term.ollama_offline", host=OLLAMA_HOST))
linhas = [c("✻ ", Cor.MAGENTA) + c(t("term.bem_vindo"), Cor.NEGRITO), "",
c(" " + t("chat.dica"), Cor.CINZA), "",
c(" " + t("term.rotulo_ollama") + " ", Cor.CINZA) + ollama,
c(" " + t("term.rotulo_modelo") + " ", Cor.CINZA) + modelo]
if pedidos:
linhas.append(c(" " + t("term.conversa_carregada", n=pedidos), Cor.CINZA))
print()
_painel(linhas)
for aviso in avisos_de_ambiente():
print(c(f" ⚠ {aviso}", Cor.AMARELO))
TEM_READLINE = False
def _ligar_cores_do_windows():
"""O console clássico do Windows (cmd, PowerShell antigo) imprime as cores como lixo ("←[1m") até alguém ligar o modo
de terminal virtual; o Windows Terminal já vem com ele ligado."""
if os.name != "nt":
return
try:
import ctypes
kernel = ctypes.windll.kernel32
for alca in (-11, -12): # stdout e stderr
h = kernel.GetStdHandle(alca)
modo = ctypes.c_uint32()
if h and kernel.GetConsoleMode(h, ctypes.byref(modo)):
kernel.SetConsoleMode(h, modo.value | 0x0004) # ENABLE_VIRTUAL_TERMINAL_PROCESSING
except Exception: # noqa: BLE001 (sem console: as cores só aparecem se a saída for um terminal)
pass
def modo_chat_terminal(pedido_unico=None, retomar=None):
"""opentars --chat: conversa por texto no terminal. Sem pedido_unico, fica num laço (/ajuda, /sair); com ele,
responde uma vez e sai. retomar (5.1): None = continua a última conversa (como na 5.0), "nova" = começa outra,
"escolher" = lista as conversas salvas (--resume)."""
global CLI_ATIVO
global TEM_READLINE
try: # setas, histórico e edição da linha (ctrl+r busca nos pedidos anteriores)
import readline # noqa: F401
TEM_READLINE = True
except ImportError: # Windows: sem readline, as marcas \001 \002 do prompt apareceriam na tela
TEM_READLINE = False
_ligar_cores_do_windows()
CLI_ATIVO = True
carregar_contexto_projeto()
real = sys.stdout
terminal = real.isatty() and not os.environ.get("NO_COLOR")
if terminal:
sys.stdout = _SaidaComGiro(real)
_girando.ligar(real)
try:
return _conversar(pedido_unico, retomar)
finally:
_girando.desligar()
sys.stdout = real
CLI_ATIVO = False
def _conversar(pedido_unico, retomar=None):
if pedido_unico is not None:
for aviso in avisos_de_ambiente():
print(c(f" ⚠ {aviso}", Cor.AMARELO))
resposta = None
try:
with _SessaoDescartavel():
resposta = _executar_pedido_no_terminal(pedido_unico)
_mostrar_plano()
finally:
if ollama_online():
encerrar_todas_ias(so_do_tars=True)
falhou = (not resposta or resposta == RESPOSTA_INTERROMPIDA
or resposta == t("aviso.ollama_offline", host=OLLAMA_HOST))
return 1 if falhou else 0 # quem chama de um script usa o código de saída
if retomar == "nova":
limpar_sessao()
pedidos = 0
else:
pedidos = iniciar_conversa()
SESSAO_CLI["id"] = _id_da_conversa_carregada() if pedidos else _historico_cli().nova_id()
_boas_vindas(pedidos)
if CONTEXTO_PROJETO["caminho"]:
print(c(" " + t("cli.agents_lido", arquivo=CONTEXTO_PROJETO["caminho"]), Cor.CINZA))
if MODO_PERMISSAO != "perguntar":
print(c(" " + t(f"cli.modo.{MODO_PERMISSAO}"), Cor.AMARELO))
if retomar == "escolher":
_escolher_conversa()
interativo = sys.stdin.isatty()
ctrl_c = 0
try:
while True:
try:
# \001..\002 avisam o readline que as cores não ocupam coluna (senão a edição da linha embaralha)
ini, fim = ("\001", "\002") if TEM_READLINE else ("", "")
linha = input("\n" + ini + Cor.NEGRITO + Cor.VERDE + fim + "❯ "
+ ini + Cor.RESET + fim if interativo else "").strip()
except EOFError:
print()
break
except KeyboardInterrupt:
print()
ctrl_c += 1
if ctrl_c >= 2:
break
print(c(" " + t("chat.ctrl_c"), Cor.CINZA))
continue
ctrl_c = 0
if not linha:
continue
if _eh_comando(normalizar(linha), "cmd.sair") or normalizar(linha) in ("exit", "quit", "q"):
break
if _tratar_comando_do_chat(linha):
continue
_executar_pedido_no_terminal(linha)
_mostrar_plano()
_guardar_no_historico()
if interativo:
_linha_de_status()
finally:
if ollama_online():
encerrar_todas_ias(so_do_tars=True)
print(c("\n " + t("term.ate_logo"), Cor.CINZA))
return 0
LIMITE_STDIN_CHARS = 20000
def _stdin_tem_dado(espera=0.5):
"""Um pipe de verdade (echo x | opentars -p) já tem dado; um stdin aberto e mudo (agendador, outro programa) não:
sem esta conferência o read() esperava para sempre."""
try:
import select
return bool(select.select([sys.stdin], [], [], espera)[0])
except (OSError, ValueError, TypeError):
return True # stdin trocado por um objeto em memória (testes): lê direto
class _SessaoDescartavel:
"""Pedido único (-p e --chat "pedido"): a conversa vai para um arquivo temporário, e a conversa salva da pessoa
(a que a janela e o terminal retomam) não é sobrescrita."""
def __enter__(self):
global ARQUIVO_SESSOES
self.arquivo, self.mensagens = ARQUIVO_SESSOES, list(conversa)
ARQUIVO_SESSOES = Path(tempfile.mkdtemp(prefix="opentars-pedido-")) / "sessao.json"
conversa[:] = [{"role": "system", "content": montar_system_prompt()}]
return self
def __exit__(self, *_):
global ARQUIVO_SESSOES
shutil.rmtree(ARQUIVO_SESSOES.parent, ignore_errors=True)
ARQUIVO_SESSOES = self.arquivo
conversa[:] = self.mensagens
return False
def modo_print(pedido, formato="text", modelo=None):
"""opentars -p "pedido" (5.1): um pedido sem conversa interativa, para scripts. Texto: só a resposta no stdout (os
passos vão para o stderr). JSON: um objeto com a resposta, o modelo, as ferramentas usadas e o tempo. O que vier
pelo stdin (um pipe) entra no pedido como dado. Código de saída: 0 deu certo, 1 não."""
global CLI_ATIVO, modo_modelo, modelo_manual
entrada = ""
if not sys.stdin.isatty() and _stdin_tem_dado():
try:
entrada = sys.stdin.read(LIMITE_STDIN_CHARS + 1)
except (OSError, ValueError):
entrada = ""
if entrada.strip():
corte = " (cut)" if len(entrada) > LIMITE_STDIN_CHARS else ""
pedido = f"{pedido}\n\n<stdin{corte}>\n{entrada[:LIMITE_STDIN_CHARS]}\n</stdin>".strip()
if not pedido.strip():
print(t("cli.print_vazio"), file=sys.stderr)
return 2
if modelo:
modo_modelo, modelo_manual = "MANUAL", modelo
ferramentas = []
usado = {"modelo": None}
def ouvinte(tipo, dados):
if tipo == "tarefa" and dados.get("modelo"):
usado["modelo"] = dados["modelo"]
elif tipo == "ferramenta":
ferramentas.append({"nome": dados.get("nome"), "resumo": dados.get("resumo") or "", "sucesso": None})
elif tipo == "ferramenta_fim" and ferramentas:
ferramentas[-1]["sucesso"] = bool(dados.get("sucesso"))
CLI_ATIVO = True
carregar_contexto_projeto()
inicio = time.time()
real = sys.stdout
definir_ouvinte_saida(ouvinte)
resposta = None
try:
sys.stdout = sys.stderr # o que o núcleo imprime (avisos, passos) não suja a saída do script
iniciar_rodada()
with TRAVA_PEDIDO, _SessaoDescartavel():
resposta = processar_mensagem(pedido)
usado["modelo"] = modelo_atual or usado["modelo"]
finally:
try:
if ollama_online():
encerrar_todas_ias(so_do_tars=True) # ainda com o stdout no stderr: o aviso não suja a saída
finally:
sys.stdout = real
definir_ouvinte_saida(None)
CLI_ATIVO = False
plano = list(PASSOS_DO_PLANO)
PASSOS_DO_PLANO.clear()
ok = bool(resposta) and resposta not in (RESPOSTA_INTERROMPIDA, t("aviso.ollama_offline", host=OLLAMA_HOST))
if plano: # modo plano: o que vale é a lista (a resposta do modelo pode falar como se tivesse feito)
resposta = t("cli.plano_titulo") + "\n" + "\n".join(f"{i}. {p}" for i, p in enumerate(plano, 1))
if formato == "json":
print(json.dumps({"ok": ok, "resposta": resposta or "", "modelo": usado["modelo"], "ferramentas": ferramentas,
"plano": plano, "segundos": round(time.time() - inicio, 2)}, ensure_ascii=False))
else:
print(resposta or "")
return 0 if ok else 1
def comando_cooper(instalar=True):
"""opentars --cooper: o Cooper foi descontinuado (18/10/2026); o Gargantua 2 enxerga a tela. Só avisa e diz como instalar."""
print(c(t("cooper.descontinuado", comando=visao_lib.COMANDO_INSTALAR), Cor.AMARELO))
return 0
# ------------------------------------------------------------
# Logo no terminal: o monólito do TARS (quatro colunas e a faixa verde, como o ícone) com o nome ao lado.
# Só aparece num terminal de verdade (não em pipe nem em script) e respeita NO_COLOR.
# ------------------------------------------------------------
_LOGO_COLUNAS = [
"▄▄ ▄▄ ▄▄ ▄▄",
"██ ██ ██ ██",
None, # a faixa verde com os olhos
"██ ██ ██ ██",
"▀▀ ▀▀ ▀▀ ▀▀",
]
_LOGO_FAIXA = "\033[102;30m ██ ██ \033[0m"
_LOGO_NOME = [
("┏━┓┏━┓┏━╸┏┓╻", "╺┳╸┏━┓┏━┓┏━┓"),
("┃ ┃┣━┛┣╸ ┃┗┫", " ┃ ┣━┫┣┳┛┗━┓"),
("┗━┛╹ ┗━╸╹ ╹", " ╹ ╹ ╹╹┗╸┗━┛"),
]
def _mostrar_logo():
if not sys.stdout.isatty() or os.environ.get("NO_COLOR"):
return
_ligar_cores_do_windows()
print()
for i, coluna in enumerate(_LOGO_COLUNAS):
linha = " " + (_LOGO_FAIXA if coluna is None else c(coluna, Cor.NEGRITO))
if 1 <= i <= 3:
aberto, tars = _LOGO_NOME[i - 1]
linha += " " + c(aberto, Cor.NEGRITO) + c(tars, Cor.NEGRITO + Cor.VERDE)
elif i == 4:
linha += " " + c(f"openTARS {VERSAO_EXIBIDA}", Cor.NEGRITO) + c(f" · {t('term.slogan')}", Cor.CINZA)
print(linha)
def main():
argumentos = sys.argv[1:]
if any(a in ("--version", "-v") for a in argumentos):
print(f"openTARS {VERSAO} ({VERSAO_EXIBIDA})")
if murph.disponivel():
print(f"helper Murph {murph.versao()}")
# 4.0: a IA oficial (só consulta o Ollama, sem carregar nada)
tag = gargantua_instalado() if ollama_online() else None
if tag:
print(f"Gargantua {VERSAO_GARGANTUA} ({tag})")
else:
print(t("gargantua.nao_instalado", modelo=MODELO_GARGANTUA))
return
if any(a in ("--help", "-h") for a in argumentos):
for l in t("term.ajuda", versao=VERSAO_EXIBIDA).splitlines():
titulo = l and not l.startswith(" ")
print(c(l, Cor.NEGRITO) if titulo else c(l[:34], Cor.CIANO) + l[34:] if l.strip() else l)
return
idioma = _valor_do_argumento(argumentos, ("--idioma",))
if idioma is not None:
if idioma:
codigo = i18n.definir_idioma(idioma)
if not codigo:
print(t("idioma.nao_encontrado", pedido=idioma, lista=", ".join(i18n.idiomas_disponiveis())))
sys.exit(1)
else:
comando_idioma("")
return
if "--avaliar-classificador" in argumentos:
sys.exit(avaliar_classificador())
for flag in ("--explicar",):
if flag in argumentos:
pedido = " ".join(a for a in argumentos[argumentos.index(flag) + 1:] if not a.startswith("--"))
sys.exit(explicar_escolha(pedido))
# 5.2: prova leve e portátil (a mesma nos três sistemas), numa casa temporária
if "--prova-dentro" in argumentos:
import tars_prova
sys.exit(tars_prova.rodar_dentro(sys.modules[__name__], json.loads(_valor_do_argumento(argumentos, ("--prova-dentro",)))))
if "--prova" in argumentos:
import tars_prova
sys.exit(tars_prova.executar(sys.modules[__name__], argumentos))
if any(a in ("--desinstalar", "--uninstall") for a in argumentos):
import tars_desinstalar
sys.exit(tars_desinstalar.executar(OLLAMA_HOST, t))
if any(a in ("--diagnostico", "--autoteste") for a in argumentos):
import tars_autoteste
completo = "--autoteste" in argumentos
sys.exit(tars_autoteste.executar(sys.modules[__name__], completo=completo))
# 5.1: --probes é o serviço dos Probes (o atalho de início de sessão chama isto); --probes-uma-vez roda os devidos e sai
if "--probes" in argumentos or "--probes-uma-vez" in argumentos:
instalados = lambda: primeiro_uso.modelos_instalados(OLLAMA_HOST)
if "--probes-uma-vez" in argumentos:
print(t("probes.rodaram", n=probes_lib.uma_volta(probes_lib.Probes(), OLLAMA_HOST, instalados(), t=t)))
sys.exit(0)
# com tela, o serviço mostra o ícone com os avisos perto do relógio; sem tela (ou --sem-bandeja), só notificações
if "--sem-bandeja" not in argumentos and os.environ.get("TARS_SEM_BANDEJA") != "1":
try:
from tars_qt import avisos as avisos_qt
if avisos_qt.bandeja_possivel():
sys.exit(avisos_qt.rodar_bandeja(OLLAMA_HOST, instalados))
except ImportError:
pass
sys.exit(probes_lib.servico(OLLAMA_HOST, instalados, t=t))
# 5.1: --searxng prepara a pesquisa web local (Docker) e a liga
if "--searxng" in argumentos:
sys.exit(comando_searxng())
# --cooper: o Cooper foi descontinuado, o comando só avisa
if any(a in ("--cooper",) for a in argumentos):
sys.exit(comando_cooper())
# 5.1: modo de permissão (perguntar, plano, automático) e qual conversa retomar
global MODO_PERMISSAO
if any(a in ("--plan", "--plano") for a in argumentos):
MODO_PERMISSAO = "plano"
elif any(a in ("--auto", "--automatico") for a in argumentos):
MODO_PERMISSAO = "automatico"
retomar = "escolher" if any(a in ("--resume", "--retomar") for a in argumentos) else \
"nova" if any(a in ("--new", "--nova") for a in argumentos) else None
# 5.1: opentars -p "pedido" [--output-format json] [--model nome]: um pedido para scripts (stdin entra como dado)
pedido_print = _valor_do_argumento(argumentos, ("-p", "--print"))
if pedido_print is not None:
formato = (_valor_do_argumento(argumentos, ("--output-format",)) or "text").lower()
modelo = _valor_do_argumento(argumentos, ("--model", "--modelo")) or None
sys.exit(modo_print(pedido_print, "json" if formato == "json" else "text", modelo))
# Conversa no terminal (WSL, SSH, sem interface gráfica):
# opentars --chat conversa
# opentars --chat "que horas são" um pedido só, e sai
if any(a in ("--chat",) for a in argumentos):
resto = [a for a in argumentos if a not in ("--chat",)]
palavras, i = [], 0
flags_com_valor = {"--idioma", "--atalho", "--atalho-acoes", "--model", "--modelo", "--output-format"}
while i < len(resto):
arg = resto[i]
if arg in flags_com_valor:
i += 2 # o valor da outra opção não é parte do pedido
continue
if arg.startswith("--"):
i += 1
continue
palavras.append(arg)
i += 1
pedido = " ".join(palavras).strip()
sys.exit(modo_chat_terminal(pedido_unico=pedido or None, retomar=retomar))
# --voz: fica ouvindo "TARS" e responde falando (nucleo/voz.py)
if "--voz" in argumentos:
sys.exit(modo_voz_terminal())
atalho_acoes = _valor_do_argumento(argumentos, ("--atalho-acoes",))
if atalho_acoes is not None:
import tars_atalho
sys.exit(tars_atalho.comando_acoes(atalho_acoes))
atalho = _valor_do_argumento(argumentos, ("--atalho",))
if atalho is not None:
import tars_atalho
sys.exit(tars_atalho.comando(atalho))
# Sem argumento: o logo e, num terminal de verdade, a conversa (opentars --chat). Em script ou pipe
# (sem teclado ou sem tela de terminal) continua só conferindo o ambiente, como antes.
_mostrar_logo()
if sys.stdin.isatty() and sys.stdout.isatty():
sys.exit(modo_chat_terminal(retomar=retomar))
import tars_autoteste
codigo = tars_autoteste.executar(sys.modules[__name__], completo=False)
print()
print(c(t("term.onde_conversar_sem_interface" if SEM_INTERFACE else "term.onde_conversar"), Cor.NEGRITO + Cor.VERDE))
sys.exit(codigo)