# Plano de Implementacao: CLI TecJustica

## Visao Geral

Criar pacote `cli/` no mesmo repo com Typer app que se conecta ao MCP server via HTTP/SSE. Cada tool do servidor vira um comando CLI. Auth via config file local. Output Rich para humanos, `--json` para maquinas.

## Fase 1: Esqueleto + MCPClient

Criar a infraestrutura base: MCPClient (protocolo MCP via httpx), config manager, e Typer app minimo.

### Tarefas

- [ ] Criar `cli/__init__.py`
- [ ] Criar `cli/client.py` — classe `MCPClient` [complexo]
  - [ ] `__init__(server_url, token)` com httpx.Client sync
  - [ ] `connect()` — initialize + notifications/initialized
  - [ ] `call_tool(name, arguments)` — lazy connect + tools/call
  - [ ] `_parse_sse(response)` — extrair JSON-RPC de text/event-stream
  - [ ] Timeout 120s, headers MCP session
- [ ] Criar `cli/config.py` — leitura/escrita de `~/.config/tecjustica/config.toml`
  - [ ] `load_config()` — le config.toml se existir
  - [ ] `save_config(data)` — escreve config.toml (cria dir se necessario)
  - [ ] `get_token()` — resolucao: env `TECJUSTICA_TOKEN` > config.toml > None
  - [ ] `get_server_url()` — default `https://mcp.tecjustica.com/mcp`
- [ ] Criar `cli/main.py` — Typer app com `--version` callback
- [ ] Criar `cli/commands/__init__.py`
- [ ] Atualizar `pyproject.toml`: entry point, dependencias, hatch build

### Detalhes Tecnicos

**MCPClient — protocolo MCP sobre HTTP:**
```python
class MCPClient:
    def __init__(self, server_url: str, token: str):
        self.server_url = server_url
        self.token = token
        self.session_id: str | None = None
        self.http = httpx.Client(
            base_url=server_url,
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json, text/event-stream",
                "Authorization": f"Bearer {token}",
            },
            timeout=120.0,
        )

    def connect(self) -> None:
        resp = self._jsonrpc("initialize", {
            "protocolVersion": "2025-03-26",
            "capabilities": {},
            "clientInfo": {"name": "tecjustica-cli", "version": "0.1.0"},
        }, id=1)
        resp.raise_for_status()
        self.session_id = resp.headers["mcp-session-id"]
        self._jsonrpc("notifications/initialized")

    def call_tool(self, name: str, arguments: dict) -> dict:
        if not self.session_id:
            self.connect()
        resp = self._jsonrpc("tools/call", {"name": name, "arguments": arguments}, id=2)
        resp.raise_for_status()
        return self._parse_sse(resp)

    def _parse_sse(self, resp: httpx.Response) -> dict:
        if "text/event-stream" in resp.headers.get("content-type", ""):
            for line in resp.text.strip().split("\n"):
                if line.startswith("data:"):
                    data = line[5:].strip()
                    if data:
                        return json.loads(data)
        return resp.json()
```

**Config file** (`~/.config/tecjustica/config.toml`):
```toml
[auth]
token = "tk_0bd3797d..."
server_url = "https://mcp.tecjustica.com/mcp"
```

**pyproject.toml — alteracoes:**
```toml
# Adicionar em dependencies:
"typer[all]>=0.12.0",
"tomli-w>=1.0.0",

# Adicionar secao:
[project.scripts]
tecjustica = "cli.main:app"

# Atualizar:
[tool.hatch.build.targets.wheel]
packages = ["tools", "services", "cli", "models"]

[tool.ruff.lint.isort]
known-first-party = ["tools", "services", "models", "auth", "cli"]
```

## Fase 2: Auth (OAuth + API key)

Autenticacao com dois metodos: OAuth via browser (mesmo fluxo do Claude Code) e API key direta.

### Tarefas

- [ ] Criar `cli/oauth.py` — fluxo OAuth completo [complexo]
  - [ ] OAuth Discovery: `GET /.well-known/oauth-authorization-server` para obter endpoints
  - [ ] Dynamic Client Registration (DCR): registrar client no `registration_endpoint`
  - [ ] Gerar `code_verifier` + `code_challenge` (PKCE S256)
  - [ ] Levantar HTTP server temporario em `localhost:8484` para callback
  - [ ] Abrir browser com `authorization_endpoint` + params OAuth
  - [ ] Capturar `authorization_code` no callback
  - [ ] Trocar code por token no `token_endpoint`
  - [ ] Salvar `access_token` + `refresh_token` no config
- [ ] Criar `cli/commands/auth.py` com subcomandos Typer
  - [ ] `tecjustica login` (sem flags) — fluxo OAuth completo via browser
  - [ ] `tecjustica login --token TOKEN` — salva API key direto no config
  - [ ] `tecjustica status` — chama `whoami` via MCPClient, mostra resultado
  - [ ] `tecjustica logout` — remove tokens do config.toml
- [ ] Registrar comandos em `cli/main.py`

### Detalhes Tecnicos

**Fluxo OAuth (tecjustica login):**
```
1. GET https://mcp.tecjustica.com/.well-known/oauth-authorization-server
   → { authorization_endpoint, token_endpoint, registration_endpoint, ... }

2. POST registration_endpoint (DCR)
   → { client_id, client_secret (opcional) }

3. Gerar PKCE:
   code_verifier = secrets.token_urlsafe(64)
   code_challenge = base64url(sha256(code_verifier))

4. Levantar http.server em localhost:8484

5. Abrir browser:
   authorization_endpoint?
     response_type=code&
     client_id=...&
     redirect_uri=http://localhost:8484/callback&
     code_challenge=...&
     code_challenge_method=S256&
     state=...

6. Usuario faz login no Supabase (email, Google, etc.)

7. Callback em localhost:8484/callback?code=ABC&state=...

8. POST token_endpoint
   grant_type=authorization_code&
   code=ABC&
   redirect_uri=http://localhost:8484/callback&
   code_verifier=...&
   client_id=...
   → { access_token, refresh_token, expires_in }

9. Salvar em config.toml
```

**Implementacao do callback server:**
```python
import http.server
import threading
import webbrowser

class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
    authorization_code: str | None = None

    def do_GET(self):
        from urllib.parse import urlparse, parse_qs
        params = parse_qs(urlparse(self.path).query)
        OAuthCallbackHandler.authorization_code = params.get("code", [None])[0]
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"Autenticado! Pode fechar esta aba.")

    def log_message(self, *args): pass  # silenciar logs

def oauth_login(server_url: str) -> dict:
    # 1. Discovery
    metadata = httpx.get(f"{server_url}/.well-known/oauth-authorization-server").json()

    # 2. DCR
    dcr_resp = httpx.post(metadata["registration_endpoint"], json={
        "client_name": "tecjustica-cli",
        "redirect_uris": ["http://localhost:8484/callback"],
        "grant_types": ["authorization_code"],
        "response_types": ["code"],
        "token_endpoint_auth_method": "none",
    })
    client_id = dcr_resp.json()["client_id"]

    # 3. PKCE
    code_verifier = secrets.token_urlsafe(64)
    code_challenge = base64url_sha256(code_verifier)
    state = secrets.token_urlsafe(32)

    # 4. Server local
    server = http.server.HTTPServer(("localhost", 8484), OAuthCallbackHandler)
    thread = threading.Thread(target=server.handle_request)
    thread.start()

    # 5. Abrir browser
    auth_url = f"{metadata['authorization_endpoint']}?..."
    webbrowser.open(auth_url)

    # 6. Aguardar callback
    thread.join(timeout=120)
    code = OAuthCallbackHandler.authorization_code

    # 7. Trocar code por token
    token_resp = httpx.post(metadata["token_endpoint"], data={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": "http://localhost:8484/callback",
        "code_verifier": code_verifier,
        "client_id": client_id,
    })
    return token_resp.json()  # { access_token, refresh_token, expires_in }
```

**Config file apos OAuth** (`~/.config/tecjustica/config.toml`):
```toml
[auth]
token = "eyJhbGciOi..."          # access_token (JWT)
refresh_token = "abc123..."       # para renovar quando expirar
client_id = "..."                 # do DCR, necessario para refresh
server_url = "https://mcp.tecjustica.com/mcp"
method = "oauth"                  # ou "api_key"
```

**Resolucao de token (em todas as commands):**
1. Env var `TECJUSTICA_TOKEN`
2. `~/.config/tecjustica/config.toml` → `auth.token`
3. Se token JWT expirado + refresh_token existe → refresh automatico
4. Erro: "Nenhum token configurado. Rode: tecjustica login"

**Fluxo API key (tecjustica login --token):**
```python
# Simples: salva direto, sem OAuth
save_config({"auth": {"token": token, "method": "api_key", "server_url": server_url}})
```

## Fase 3: Comandos Core (validar fluxo completo)

3 comandos iniciais para validar toda a stack: MCPClient → MCP server → resposta formatada.

### Tarefas

- [ ] Criar `cli/commands/utilidades.py` — comando `whoami`
- [ ] Criar `cli/commands/navegacao.py` — comandos `listar-processos` e `visao-geral`
- [ ] Criar `cli/formatting.py` — formatadores Rich [complexo]
  - [ ] `format_whoami(data)` — painel key-value
  - [ ] `format_listar_processos(data)` — tabela (Numero, Classe, Tribunal, Partes)
  - [ ] `format_visao_geral(data)` — painel com metadados + tabela partes
- [ ] Registrar comandos em `cli/main.py`
- [ ] Testar contra producao: `tecjustica whoami`, `tecjustica listar-processos`, `tecjustica visao-geral CNJ`

### Detalhes Tecnicos

**Padrao de um comando:**
```python
@app.command()
def whoami(
    json_output: Annotated[bool, typer.Option("--json", help="Saida JSON")] = False,
) -> None:
    """Mostra identidade do usuario autenticado."""
    token = get_token()
    if not token:
        typer.echo("Erro: nenhum token. Rode: tecjustica auth login")
        raise typer.Exit(1)
    client = MCPClient(get_server_url(), token)
    result = client.call_tool("whoami", {})
    if json_output:
        typer.echo(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        format_whoami(result)
```

**Helper para extrair token + criar client (evitar repeticao):**
```python
def get_client() -> MCPClient:
    token = get_token()
    if not token:
        console.print("[red]Nenhum token configurado.[/red]")
        console.print("Rode: tecjustica auth login --token SEU_TOKEN")
        raise typer.Exit(1)
    return MCPClient(get_server_url(), token)
```

## Fase 4: Todos os Comandos Restantes

Implementar os 12 comandos restantes seguindo o padrao estabelecido na Fase 3.

### Tarefas

- [ ] Navegacao: `ls-docs`, `ls-movs`, `read-doc` (em `cli/commands/navegacao.py`)
- [ ] Busca: `grep-docs`, `grep-movs`, `glob-docs`, `localizar` (em `cli/commands/busca.py`)
- [ ] Analise: `analisar`, `precedentes` (em `cli/commands/analise.py`) [complexo]
  - [ ] Spinner Rich para `analisar` (pode levar 60-120s)
  - [ ] Args `--orgaos` e `--tipos` como lista para `precedentes`
- [ ] Estatisticas: `stats-docs` (em `cli/commands/estatisticas.py`)
- [ ] Utilidades: `calc`, `agora` (em `cli/commands/utilidades.py`)
- [ ] Formatadores Rich para cada resposta em `cli/formatting.py`

### Detalhes Tecnicos

**Mapeamento completo de comandos:**

| CLI Command | MCP Tool | Args |
|---|---|---|
| `listar-processos` | `listar_processos` | `--busca TEXT` |
| `visao-geral CNJ` | `visao_geral_processo` | positional `numero_processo` |
| `ls-docs CNJ` | `ls_documentos` | `--fonte --offset --limit` |
| `ls-movs CNJ` | `ls_movimentacoes` | `--fonte --offset --limit` |
| `read-doc ID` | `read_documento` | `--offset --max-chars` |
| `grep-docs CNJ PADRAO` | `grep_documentos` | `--fonte` |
| `grep-movs CNJ PADRAO` | `grep_movimentacoes` | `--fonte` |
| `glob-docs CNJ PADRAO` | `glob_documentos` | `--fonte` |
| `localizar ID TERMO` | `localizar_no_documento` | — |
| `stats-docs CNJ` | `stats_documentos` | `--fonte` |
| `analisar CNJ` | `analisar` | `--pergunta --perspectiva` |
| `precedentes BUSCA` | `buscar_precedentes` | `--orgaos --tipos` |
| `calc MODO` | `calculadora` | `--expressao --data-intimacao --dias` |
| `agora` | `data_hora_atual` | `--timezone` |
| `whoami` | `whoami` | — |

**Spinner para analisar:**
```python
with console.status("[bold green]Analisando processo...") as status:
    result = client.call_tool("analisar", arguments)
```

**Lista args (precedentes --orgaos):**
```python
@app.command()
def precedentes(
    busca: str,
    orgaos: Annotated[list[str] | None, typer.Option("--orgao")] = None,
    tipos: Annotated[list[str] | None, typer.Option("--tipo")] = None,
): ...
```

## Fase 5: Polish

Refinamentos finais: erro handling, UX, flags globais.

### Tarefas

- [ ] Flag `--json` global via Typer callback (evitar repetir em cada comando)
- [ ] Error handling centralizado [complexo]
  - [ ] 401 → "Token invalido ou expirado. Rode: tecjustica auth login"
  - [ ] Connection error → "Servidor indisponivel. Verifique sua conexao"
  - [ ] MCP session error → retry automatico (novo connect)
  - [ ] JSON-RPC error → mostrar mensagem do servidor
- [ ] `tecjustica --version` mostra versao do pyproject.toml
- [ ] Mensagem de boas-vindas no primeiro uso (se nao tem config)
- [ ] Verificar que `uv run ruff check . && uv run ruff format --check . && uv run mypy .` passa limpo

### Detalhes Tecnicos

**Error handling centralizado:**
```python
def handle_mcp_error(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                console.print("[red]Token invalido ou expirado.[/red]")
                console.print("Rode: tecjustica auth login --token SEU_TOKEN")
            else:
                console.print(f"[red]Erro HTTP {e.response.status_code}[/red]")
            raise typer.Exit(1)
        except httpx.ConnectError:
            console.print("[red]Servidor indisponivel.[/red]")
            raise typer.Exit(1)
    return wrapper
```

## Arquivos a criar/modificar

### Novos arquivos
- `cli/__init__.py`
- `cli/main.py`
- `cli/client.py`
- `cli/config.py`
- `cli/oauth.py`
- `cli/formatting.py`
- `cli/commands/__init__.py`
- `cli/commands/auth.py`
- `cli/commands/navegacao.py`
- `cli/commands/busca.py`
- `cli/commands/analise.py`
- `cli/commands/estatisticas.py`
- `cli/commands/utilidades.py`

### Arquivos modificados
- `pyproject.toml` — entry point, dependencias, hatch build, isort
- `requirements.txt` — adicionar typer[all], tomli-w
