claudio-agent 2.0.0__tar.gz
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.
- claudio_agent-2.0.0/.claudio/skills/skill.md +1604 -0
- claudio_agent-2.0.0/MANIFEST.in +2 -0
- claudio_agent-2.0.0/PKG-INFO +65 -0
- claudio_agent-2.0.0/README.md +40 -0
- claudio_agent-2.0.0/claudio.md +107 -0
- claudio_agent-2.0.0/claudio.py +1830 -0
- claudio_agent-2.0.0/claudio_agent.egg-info/PKG-INFO +65 -0
- claudio_agent-2.0.0/claudio_agent.egg-info/SOURCES.txt +12 -0
- claudio_agent-2.0.0/claudio_agent.egg-info/dependency_links.txt +1 -0
- claudio_agent-2.0.0/claudio_agent.egg-info/entry_points.txt +2 -0
- claudio_agent-2.0.0/claudio_agent.egg-info/requires.txt +2 -0
- claudio_agent-2.0.0/claudio_agent.egg-info/top_level.txt +1 -0
- claudio_agent-2.0.0/pyproject.toml +46 -0
- claudio_agent-2.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,1604 @@
|
|
|
1
|
+
# claudio - Agente Autônomo de Programação via Terminal
|
|
2
|
+
|
|
3
|
+
## Descrição
|
|
4
|
+
Skill para recriar o **claudio**, um agente autônomo de programação que roda no terminal. Usa API OpenAI com `rich` para interface. Permite criar projetos, analisar código, debugar, editar/refatorar arquivos, buscar na web, comandos git, modo autônomo multi-passo, troca de modelo, auto-instalar dependências, skills, RULES, chat normal e persistência de sessão.
|
|
5
|
+
|
|
6
|
+
## Estrutura do Projeto
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
opoenClaudio/
|
|
10
|
+
├── .env # Chave da API OpenAI
|
|
11
|
+
├── claudio.py # Script principal do agente
|
|
12
|
+
├── claudio.md # Documentação
|
|
13
|
+
├── opencode.md # Documentação opencode
|
|
14
|
+
├── README.md # Guia de início rápido
|
|
15
|
+
├── pyproject.toml # Pacote Python (pip install claudio-agent)
|
|
16
|
+
├── requirements.txt # Dependências Python
|
|
17
|
+
├── MANIFEST.in # Inclui .claudio/ e claudio.md no pacote
|
|
18
|
+
├── .gitignore # Exclui .env, __pycache__, etc.
|
|
19
|
+
├── package.json # Scripts locais (privado, sem publish)
|
|
20
|
+
├── scripts/
|
|
21
|
+
│ ├── build-exe.js # Build PyInstaller
|
|
22
|
+
│ └── publish-pypi.py # Publica no PyPI
|
|
23
|
+
├── .claudio/
|
|
24
|
+
│ ├── skills/
|
|
25
|
+
│ │ └── skill.md # Esta skill (auto-reconstrução)
|
|
26
|
+
│ └── RULES/ # Regras carregadas no contexto (opcional)
|
|
27
|
+
├── .claudio_history.json # Histórico de sessão (gerado automaticamente)
|
|
28
|
+
└── .opencode/
|
|
29
|
+
└── SKILL.md # Skill opencode (cópia)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Dependências
|
|
33
|
+
|
|
34
|
+
- Python 3.10+
|
|
35
|
+
- `openai` - Cliente da API OpenAI
|
|
36
|
+
- `rich` - Interface estilizada no terminal
|
|
37
|
+
|
|
38
|
+
Instalação:
|
|
39
|
+
```bash
|
|
40
|
+
pip install openai rich
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Arquivos Necessários
|
|
44
|
+
|
|
45
|
+
### 1. `.env` — Chave da API
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
API_KEY = "sk-proj-..."
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> ⚠️ Substitua pela sua chave real da OpenAI. A API key também pode ser definida via variável de ambiente `API_KEY`.
|
|
52
|
+
|
|
53
|
+
### 2. `claudio.py` — Script Principal
|
|
54
|
+
|
|
55
|
+
[Ver conteúdo completo no final](#código-completo-do-claudiopy)
|
|
56
|
+
|
|
57
|
+
### 3. Comando no PATH (opcional)
|
|
58
|
+
|
|
59
|
+
Criar `claudio.cmd` em um diretório que esteja no `PATH`:
|
|
60
|
+
```cmd
|
|
61
|
+
@echo off
|
|
62
|
+
python "C:\Users\Claudio\Desktop\opoenClaudio\claudio.py" %*
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Funcionalidades
|
|
66
|
+
|
|
67
|
+
| Comando | Descrição |
|
|
68
|
+
|---------|-----------|
|
|
69
|
+
| `analisar projeto` | Analisa todo o projeto |
|
|
70
|
+
| `analisar <arquivo/pasta>` | Analisa um alvo específico |
|
|
71
|
+
| `listar` / `ls` | Lista arquivos da pasta atual |
|
|
72
|
+
| `ler <arquivo>` | Mostra o conteúdo de um arquivo |
|
|
73
|
+
| `editar <arquivo> <desc>` | Edita arquivo via IA (mostra diff) |
|
|
74
|
+
| `refatorar <arquivo>` | Refatora código (mostra diff) |
|
|
75
|
+
| `debug <arquivo> <erro>` | Debuga um arquivo com erro |
|
|
76
|
+
| `executar <comando>` | Executa comando no terminal |
|
|
77
|
+
| `git <args>` | Executa comandos git |
|
|
78
|
+
| `buscar <query>` | Busca na internet |
|
|
79
|
+
| `site <url>` | Acessa conteúdo de uma URL |
|
|
80
|
+
| `crie/criar/gere/faça/desenvolva <desc>` | Gera um projeto completo via IA |
|
|
81
|
+
| `autonomo/missao <desc>` | Modo autônomo multi-passo |
|
|
82
|
+
| `modelo <nome>` | Troca o modelo GPT |
|
|
83
|
+
| `config` | Altera chave API e modelo |
|
|
84
|
+
| `instalar/deps` | Auto-instala dependências Python |
|
|
85
|
+
| `salvar` | Salva o histórico da sessão |
|
|
86
|
+
| `carregar` | Carrega a última sessão |
|
|
87
|
+
| `skill <nome>` | Executa skill (do .claudio/skills/ ou caminho) |
|
|
88
|
+
| `skills` | Lista skills disponíveis |
|
|
89
|
+
| `executar skill <nome>` | Executa uma skill (cria novo projeto) |
|
|
90
|
+
| `aplicar skill <nome>` | Aplica skill no projeto existente |
|
|
91
|
+
| `/help` | Mostra ajuda |
|
|
92
|
+
| `/exit` | Sai do programa |
|
|
93
|
+
| *(qualquer outra coisa)* | Chat normal com a IA |
|
|
94
|
+
|
|
95
|
+
## Como Reconstruir
|
|
96
|
+
|
|
97
|
+
1. Crie a pasta do projeto
|
|
98
|
+
2. Crie o arquivo `.env` com sua chave da OpenAI
|
|
99
|
+
3. Crie `claudio.py` com o código completo abaixo
|
|
100
|
+
4. Instale as dependências: `pip install openai rich`
|
|
101
|
+
5. Execute: `python claudio.py` ou `claudio` (se instalado via pip)
|
|
102
|
+
6. Na primeira execução: escolha o modelo e insira sua chave
|
|
103
|
+
7. Coloque skills em `.claudio/skills/` ou `~/.claudio/skills/`
|
|
104
|
+
|
|
105
|
+
### Onde o agente busca arquivos
|
|
106
|
+
|
|
107
|
+
O agente carrega `claudio.md` e `.claudio/` (skills/rules) nesta ordem:
|
|
108
|
+
1. **WORKSPACE** (diretório atual) — maior prioridade
|
|
109
|
+
2. **PACKAGE_DIR** (onde `claudio.py` está instalado)
|
|
110
|
+
3. **~/.claudio/** (diretório home do usuário) — fallback
|
|
111
|
+
|
|
112
|
+
## Código Completo do `claudio.py`
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import os
|
|
116
|
+
import sys
|
|
117
|
+
import json
|
|
118
|
+
import re
|
|
119
|
+
import subprocess
|
|
120
|
+
import io
|
|
121
|
+
import datetime
|
|
122
|
+
import urllib.request
|
|
123
|
+
import urllib.parse
|
|
124
|
+
from pathlib import Path
|
|
125
|
+
from openai import OpenAI
|
|
126
|
+
|
|
127
|
+
from rich.console import Console
|
|
128
|
+
from rich.markdown import Markdown
|
|
129
|
+
from rich.panel import Panel
|
|
130
|
+
from rich.syntax import Syntax
|
|
131
|
+
from rich.prompt import Prompt
|
|
132
|
+
from rich.status import Status
|
|
133
|
+
from rich.table import Table
|
|
134
|
+
from rich.style import Style
|
|
135
|
+
from rich import box
|
|
136
|
+
|
|
137
|
+
if sys.platform == "win32":
|
|
138
|
+
os.system("chcp 65001 > nul")
|
|
139
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
140
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
|
141
|
+
|
|
142
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
143
|
+
HOME_DIR = Path.home()
|
|
144
|
+
CONFIG_DIR = HOME_DIR / ".claudio"
|
|
145
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
146
|
+
|
|
147
|
+
AVAILABLE_MODELS = [
|
|
148
|
+
"gpt-4.1-mini", "gpt-4o-mini", "gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"
|
|
149
|
+
]
|
|
150
|
+
MAX_TOKENS = 8000
|
|
151
|
+
TEMPERATURE = 0.1
|
|
152
|
+
MAX_FILE_SIZE = 500000
|
|
153
|
+
MAX_CONTEXT_CHARS = 400000
|
|
154
|
+
MAX_FILE_PREVIEW = 2000
|
|
155
|
+
HISTORY_FILE = Path(".claudio_history.json")
|
|
156
|
+
WORKSPACE = Path.cwd()
|
|
157
|
+
|
|
158
|
+
console = Console()
|
|
159
|
+
|
|
160
|
+
def load_config():
|
|
161
|
+
if CONFIG_FILE.exists():
|
|
162
|
+
try:
|
|
163
|
+
return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
return {}
|
|
167
|
+
|
|
168
|
+
def save_config(data):
|
|
169
|
+
try:
|
|
170
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
existing = load_config()
|
|
172
|
+
existing.update(data)
|
|
173
|
+
CONFIG_FILE.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
console.print(f"[yellow]⚠️ Erro ao salvar config: {e}[/]")
|
|
176
|
+
|
|
177
|
+
def setup_wizard():
|
|
178
|
+
console.print(Panel(
|
|
179
|
+
"[bold]Bem-vindo ao claudio![/]\n\n"
|
|
180
|
+
"Precisamos configurar sua chave da OpenAI antes de começar.\n"
|
|
181
|
+
"[dim]Você pode obter uma chave em: https://platform.openai.com/api-keys[/]",
|
|
182
|
+
title="[bold cyan]⚙️ Configuração Inicial[/]",
|
|
183
|
+
border_style="cyan",
|
|
184
|
+
box=box.ROUNDED,
|
|
185
|
+
padding=(1, 2)
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
key = Prompt.ask("[bold yellow]🔑 Digite sua API Key da OpenAI[/]", password=True)
|
|
189
|
+
while not key or not key.startswith("sk-"):
|
|
190
|
+
console.print("[red]❌ Chave inválida. Deve começar com 'sk-'[/]")
|
|
191
|
+
key = Prompt.ask("[bold yellow]🔑 Digite sua API Key da OpenAI[/]", password=True)
|
|
192
|
+
|
|
193
|
+
save_config({"api_key": key})
|
|
194
|
+
|
|
195
|
+
console.print("[green]✅ Chave salva em ~/.claudio/config.json[/]")
|
|
196
|
+
|
|
197
|
+
console.print("\n[bold]Modelos disponíveis:[/]")
|
|
198
|
+
for i, m in enumerate(AVAILABLE_MODELS, 1):
|
|
199
|
+
console.print(f" [cyan]{i}.[/] {m}")
|
|
200
|
+
model_idx = Prompt.ask("[bold yellow]🎯 Escolha o modelo padrão[/]", choices=[str(i) for i in range(1, len(AVAILABLE_MODELS)+1)], default="1")
|
|
201
|
+
chosen_model = AVAILABLE_MODELS[int(model_idx) - 1]
|
|
202
|
+
save_config({"model": chosen_model})
|
|
203
|
+
console.print(f"[green]✅ Modelo padrão: {chosen_model}[/]")
|
|
204
|
+
|
|
205
|
+
return key, chosen_model
|
|
206
|
+
|
|
207
|
+
API_KEY = os.getenv("API_KEY")
|
|
208
|
+
MODEL = "gpt-4.1-mini"
|
|
209
|
+
|
|
210
|
+
if not API_KEY:
|
|
211
|
+
for env_path in [
|
|
212
|
+
SCRIPT_DIR / ".env",
|
|
213
|
+
Path.cwd() / ".env",
|
|
214
|
+
HOME_DIR / ".claudio.env",
|
|
215
|
+
HOME_DIR / ".claudio" / ".env"
|
|
216
|
+
]:
|
|
217
|
+
if env_path.exists():
|
|
218
|
+
try:
|
|
219
|
+
raw = env_path.read_text(encoding="utf-8", errors="ignore")
|
|
220
|
+
API_KEY = raw.split("=", 1)[1].strip().strip("\"'").strip()
|
|
221
|
+
if API_KEY:
|
|
222
|
+
break
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
if not API_KEY:
|
|
227
|
+
cfg = load_config()
|
|
228
|
+
API_KEY = cfg.get("api_key") or ""
|
|
229
|
+
MODEL = cfg.get("model") or MODEL
|
|
230
|
+
|
|
231
|
+
if not API_KEY:
|
|
232
|
+
API_KEY, MODEL = setup_wizard()
|
|
233
|
+
|
|
234
|
+
client = OpenAI(api_key=API_KEY, timeout=120)
|
|
235
|
+
|
|
236
|
+
IGNORE_DIRS = {
|
|
237
|
+
"venv", ".git", "__pycache__", "node_modules",
|
|
238
|
+
".dart_tool", "build", "dist", ".idea", ".vscode",
|
|
239
|
+
".opencode", ".claudio"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
SUPPORTED_EXTENSIONS = {
|
|
243
|
+
".py", ".js", ".ts", ".tsx", ".jsx",
|
|
244
|
+
".dart", ".html", ".css", ".json", ".md", ".txt",
|
|
245
|
+
".yaml", ".yml", ".toml", ".cfg", ".ini", ".conf",
|
|
246
|
+
".bat", ".ps1", ".sh", ".env", ".sql", ".xml",
|
|
247
|
+
".java", ".cpp", ".c", ".h", ".hpp", ".rb", ".go",
|
|
248
|
+
".rs", ".swift", ".kt", ".scala", ".php", ".r",
|
|
249
|
+
".lua", ".pl", ".pm", ".vue", ".svelte", ".astro",
|
|
250
|
+
".mjs", ".cjs", ".mts", ".cts"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
USER_STYLE = Style(color="cyan", bold=True)
|
|
254
|
+
AI_STYLE = Style(color="green", bold=True)
|
|
255
|
+
ERROR_STYLE = Style(color="red", bold=True)
|
|
256
|
+
INFO_STYLE = Style(color="yellow", bold=True)
|
|
257
|
+
SYSTEM_STYLE = Style(color="magenta", bold=True)
|
|
258
|
+
|
|
259
|
+
chat_history = []
|
|
260
|
+
|
|
261
|
+
CLAUDIO_MD = ""
|
|
262
|
+
claudio_md_path = WORKSPACE / "claudio.md"
|
|
263
|
+
if claudio_md_path.exists():
|
|
264
|
+
try:
|
|
265
|
+
CLAUDIO_MD = claudio_md_path.read_text(encoding="utf-8", errors="ignore")
|
|
266
|
+
except:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
CLAUDIO_SKILLS = []
|
|
270
|
+
CLAUDIO_SKILLS_TEXT = ""
|
|
271
|
+
CLAUDIO_RULES = ""
|
|
272
|
+
CLAUDIO_DIR = WORKSPACE / ".claudio"
|
|
273
|
+
|
|
274
|
+
if CLAUDIO_DIR.exists():
|
|
275
|
+
skills_dir = CLAUDIO_DIR / "skills"
|
|
276
|
+
if skills_dir.exists():
|
|
277
|
+
parts = []
|
|
278
|
+
for f in sorted(skills_dir.iterdir()):
|
|
279
|
+
if f.is_file() and f.suffix in SUPPORTED_EXTENSIONS:
|
|
280
|
+
try:
|
|
281
|
+
c = f.read_text(encoding='utf-8', errors='ignore')[:MAX_FILE_PREVIEW]
|
|
282
|
+
name = f.name
|
|
283
|
+
CLAUDIO_SKILLS.append({"name": name, "content": c, "path": f})
|
|
284
|
+
parts.append(f"// {f.name}\n{c}")
|
|
285
|
+
except:
|
|
286
|
+
pass
|
|
287
|
+
if parts:
|
|
288
|
+
CLAUDIO_SKILLS_TEXT = "\n\n".join(parts)
|
|
289
|
+
|
|
290
|
+
rules_dir = CLAUDIO_DIR / "RULES"
|
|
291
|
+
if rules_dir.exists():
|
|
292
|
+
parts = []
|
|
293
|
+
for f in sorted(rules_dir.iterdir()):
|
|
294
|
+
if f.is_file() and f.suffix in SUPPORTED_EXTENSIONS:
|
|
295
|
+
try:
|
|
296
|
+
parts.append(f"// {f.name}\n{f.read_text(encoding='utf-8', errors='ignore')[:MAX_FILE_PREVIEW]}")
|
|
297
|
+
except:
|
|
298
|
+
pass
|
|
299
|
+
if parts:
|
|
300
|
+
CLAUDIO_RULES = "\n\n".join(parts)
|
|
301
|
+
|
|
302
|
+
def session_save():
|
|
303
|
+
try:
|
|
304
|
+
data = {
|
|
305
|
+
"chat_history": chat_history[-200:],
|
|
306
|
+
"model": MODEL,
|
|
307
|
+
"timestamp": str(datetime.datetime.now())
|
|
308
|
+
}
|
|
309
|
+
HISTORY_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
310
|
+
except Exception as e:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
def session_load():
|
|
314
|
+
global chat_history, MODEL
|
|
315
|
+
try:
|
|
316
|
+
if HISTORY_FILE.exists():
|
|
317
|
+
data = json.loads(HISTORY_FILE.read_text(encoding="utf-8"))
|
|
318
|
+
chat_history = data.get("chat_history", [])
|
|
319
|
+
saved_model = data.get("model")
|
|
320
|
+
if saved_model and saved_model in AVAILABLE_MODELS:
|
|
321
|
+
MODEL = saved_model
|
|
322
|
+
return True
|
|
323
|
+
except:
|
|
324
|
+
pass
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
def make_claudio_context() -> str:
|
|
328
|
+
parts = []
|
|
329
|
+
if CLAUDIO_MD:
|
|
330
|
+
parts.append(f"[claudio.md]\n{CLAUDIO_MD}")
|
|
331
|
+
if CLAUDIO_SKILLS_TEXT:
|
|
332
|
+
parts.append(f"[.claudio/skills]\n{CLAUDIO_SKILLS_TEXT}")
|
|
333
|
+
if CLAUDIO_RULES:
|
|
334
|
+
parts.append(f"[.claudio/RULES]\n{CLAUDIO_RULES}")
|
|
335
|
+
if not parts:
|
|
336
|
+
return ""
|
|
337
|
+
return "\n\n---\n\n".join(parts)
|
|
338
|
+
|
|
339
|
+
def is_invalid_response(text: str):
|
|
340
|
+
if not text:
|
|
341
|
+
return True
|
|
342
|
+
if len(text) > 500000:
|
|
343
|
+
return True
|
|
344
|
+
if re.fullmatch(r"(.)\1{500,}", text):
|
|
345
|
+
return True
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
def write_file(file_path: str, content: str):
|
|
349
|
+
try:
|
|
350
|
+
if len(content) > MAX_FILE_SIZE:
|
|
351
|
+
console.print(f"[red]❌ Arquivo muito grande: {file_path}[/]")
|
|
352
|
+
return False
|
|
353
|
+
full_path = WORKSPACE / file_path
|
|
354
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
355
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
356
|
+
f.write(content)
|
|
357
|
+
console.print(f"[green]✅ Arquivo criado: [bold]{file_path}[/][/]")
|
|
358
|
+
return True
|
|
359
|
+
except Exception as e:
|
|
360
|
+
console.print(f"[red]❌ Erro ao criar arquivo: {e}[/]")
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
def read_file(file_path: str):
|
|
364
|
+
try:
|
|
365
|
+
p = Path(file_path)
|
|
366
|
+
full_path = p if p.is_absolute() else WORKSPACE / file_path
|
|
367
|
+
if not full_path.exists():
|
|
368
|
+
return None
|
|
369
|
+
with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
370
|
+
return f.read()
|
|
371
|
+
except Exception as e:
|
|
372
|
+
console.print(f"[red]❌ Erro ao ler arquivo: {e}[/]")
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
def get_project_structure():
|
|
376
|
+
structure = []
|
|
377
|
+
for root, dirs, files in os.walk(WORKSPACE):
|
|
378
|
+
dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
|
|
379
|
+
level = root.replace(str(WORKSPACE), "").count(os.sep)
|
|
380
|
+
indent = " " * 4 * level
|
|
381
|
+
structure.append(f"{indent}{Path(root).name}/")
|
|
382
|
+
subindent = " " * 4 * (level + 1)
|
|
383
|
+
for file in files:
|
|
384
|
+
structure.append(f"{subindent}{file}")
|
|
385
|
+
return "\n".join(structure)
|
|
386
|
+
|
|
387
|
+
def get_project_context():
|
|
388
|
+
context = []
|
|
389
|
+
total_chars = 0
|
|
390
|
+
|
|
391
|
+
claudio_md_path = WORKSPACE / "claudio.md"
|
|
392
|
+
if claudio_md_path.exists():
|
|
393
|
+
try:
|
|
394
|
+
c = claudio_md_path.read_text(encoding="utf-8", errors="ignore")[:MAX_FILE_PREVIEW]
|
|
395
|
+
if c:
|
|
396
|
+
context.append(f"\n\nARQUIVO: {claudio_md_path}\n\n{c}")
|
|
397
|
+
total_chars += len(c)
|
|
398
|
+
except:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
claudio_dir = WORKSPACE / ".claudio"
|
|
402
|
+
|
|
403
|
+
skills_dir = claudio_dir / "skills"
|
|
404
|
+
if skills_dir.exists():
|
|
405
|
+
for f in sorted(skills_dir.iterdir()):
|
|
406
|
+
if f.is_file() and f.suffix in SUPPORTED_EXTENSIONS:
|
|
407
|
+
if total_chars >= MAX_CONTEXT_CHARS:
|
|
408
|
+
context.append(f"\n\n... [limite de {MAX_CONTEXT_CHARS} caracteres atingido]")
|
|
409
|
+
break
|
|
410
|
+
try:
|
|
411
|
+
c = f.read_text(encoding="utf-8", errors="ignore")[:MAX_FILE_PREVIEW]
|
|
412
|
+
context.append(f"\n\nARQUIVO: {f}\n\n{c}")
|
|
413
|
+
total_chars += len(c)
|
|
414
|
+
except:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
rules_dir = claudio_dir / "RULES"
|
|
418
|
+
if rules_dir.exists():
|
|
419
|
+
for f in sorted(rules_dir.iterdir()):
|
|
420
|
+
if f.is_file() and f.suffix in SUPPORTED_EXTENSIONS:
|
|
421
|
+
if total_chars >= MAX_CONTEXT_CHARS:
|
|
422
|
+
context.append(f"\n\n... [limite de {MAX_CONTEXT_CHARS} caracteres atingido]")
|
|
423
|
+
break
|
|
424
|
+
try:
|
|
425
|
+
c = f.read_text(encoding="utf-8", errors="ignore")[:MAX_FILE_PREVIEW]
|
|
426
|
+
context.append(f"\n\nARQUIVO: {f}\n\n{c}")
|
|
427
|
+
total_chars += len(c)
|
|
428
|
+
except:
|
|
429
|
+
pass
|
|
430
|
+
|
|
431
|
+
def priority(suffix):
|
|
432
|
+
return 0 if suffix == ".py" else 1 if suffix in (".js", ".ts", ".tsx", ".jsx") else 2 if suffix in (".json", ".md", ".toml", ".yaml", ".yml") else 3
|
|
433
|
+
file_list = []
|
|
434
|
+
for root, dirs, files in os.walk(WORKSPACE):
|
|
435
|
+
dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
|
|
436
|
+
for file in files:
|
|
437
|
+
path = Path(root) / file
|
|
438
|
+
if path.suffix in SUPPORTED_EXTENSIONS and path != claudio_md_path and not str(path).startswith(str(claudio_dir)):
|
|
439
|
+
try:
|
|
440
|
+
size = path.stat().st_size
|
|
441
|
+
file_list.append((priority(path.suffix), size, path))
|
|
442
|
+
except:
|
|
443
|
+
pass
|
|
444
|
+
file_list.sort(key=lambda x: (x[0], x[1]))
|
|
445
|
+
for _, _, path in file_list:
|
|
446
|
+
try:
|
|
447
|
+
content = path.read_text(encoding="utf-8", errors="ignore")
|
|
448
|
+
content = content[:MAX_FILE_PREVIEW]
|
|
449
|
+
if total_chars + len(content) > MAX_CONTEXT_CHARS:
|
|
450
|
+
remaining = MAX_CONTEXT_CHARS - total_chars
|
|
451
|
+
if remaining > 200:
|
|
452
|
+
context.append(f"\n\nARQUIVO: {path}\n\n{content[:remaining]}")
|
|
453
|
+
context.append(f"\n\n... [limite de {MAX_CONTEXT_CHARS} caracteres atingido, {len(file_list) - len(context)} arquivos restantes]")
|
|
454
|
+
break
|
|
455
|
+
context.append(f"\n\nARQUIVO: {path}\n\n{content}")
|
|
456
|
+
total_chars += len(content)
|
|
457
|
+
except:
|
|
458
|
+
pass
|
|
459
|
+
return "".join(context)
|
|
460
|
+
|
|
461
|
+
def render_message(role: str, content: str):
|
|
462
|
+
if role == "user":
|
|
463
|
+
console.print(Panel(
|
|
464
|
+
content,
|
|
465
|
+
title="[bold cyan]Você[/]",
|
|
466
|
+
border_style="cyan",
|
|
467
|
+
box=box.ROUNDED,
|
|
468
|
+
padding=(1, 2)
|
|
469
|
+
))
|
|
470
|
+
elif role == "assistant":
|
|
471
|
+
md = Markdown(content)
|
|
472
|
+
console.print(Panel(
|
|
473
|
+
md,
|
|
474
|
+
title="[bold green]claudio[/]",
|
|
475
|
+
border_style="green",
|
|
476
|
+
box=box.ROUNDED,
|
|
477
|
+
padding=(1, 2)
|
|
478
|
+
))
|
|
479
|
+
elif role == "error":
|
|
480
|
+
console.print(Panel(
|
|
481
|
+
content,
|
|
482
|
+
title="[bold red]Erro[/]",
|
|
483
|
+
border_style="red",
|
|
484
|
+
box=box.ROUNDED,
|
|
485
|
+
padding=(1, 2)
|
|
486
|
+
))
|
|
487
|
+
elif role == "system":
|
|
488
|
+
console.print(Panel(
|
|
489
|
+
content,
|
|
490
|
+
title="[bold yellow]Sistema[/]",
|
|
491
|
+
border_style="yellow",
|
|
492
|
+
box=box.ROUNDED,
|
|
493
|
+
padding=(1, 2)
|
|
494
|
+
))
|
|
495
|
+
elif role == "info":
|
|
496
|
+
console.print(f"[yellow]ℹ️ {content}[/]")
|
|
497
|
+
elif role == "code":
|
|
498
|
+
syntax = Syntax(content, "python", theme="monokai", line_numbers=True)
|
|
499
|
+
console.print(Panel(
|
|
500
|
+
syntax,
|
|
501
|
+
title="[bold blue]Código[/]",
|
|
502
|
+
border_style="blue",
|
|
503
|
+
box=box.ROUNDED,
|
|
504
|
+
padding=(1, 2)
|
|
505
|
+
))
|
|
506
|
+
|
|
507
|
+
def normal_chat(user_message: str):
|
|
508
|
+
chat_history.append({"role": "user", "content": user_message})
|
|
509
|
+
try:
|
|
510
|
+
ctx = make_claudio_context()
|
|
511
|
+
messages = [{"role": "system", "content": f"""
|
|
512
|
+
Você é um assistente de programação.
|
|
513
|
+
REGRAS:
|
|
514
|
+
- Responda normalmente
|
|
515
|
+
- NÃO crie projetos
|
|
516
|
+
- NÃO gere JSON
|
|
517
|
+
- NÃO gere arquivos
|
|
518
|
+
- NÃO gere pastas
|
|
519
|
+
- Apenas converse
|
|
520
|
+
{ctx}
|
|
521
|
+
"""}]
|
|
522
|
+
for msg in chat_history[-20:]:
|
|
523
|
+
messages.append(msg)
|
|
524
|
+
|
|
525
|
+
with Status("[bold yellow]🤔 Pensando...", spinner="dots"):
|
|
526
|
+
response = client.chat.completions.create(
|
|
527
|
+
model=MODEL,
|
|
528
|
+
messages=messages,
|
|
529
|
+
temperature=0.3,
|
|
530
|
+
max_tokens=1000
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
content = response.choices[0].message.content
|
|
534
|
+
chat_history.append({"role": "assistant", "content": content})
|
|
535
|
+
render_message("assistant", content if content else "Sem resposta")
|
|
536
|
+
return content
|
|
537
|
+
except Exception as e:
|
|
538
|
+
error_msg = f"❌ {e}"
|
|
539
|
+
render_message("error", error_msg)
|
|
540
|
+
return error_msg
|
|
541
|
+
|
|
542
|
+
def ask_ai(prompt: str):
|
|
543
|
+
try:
|
|
544
|
+
with Status("[bold yellow]🚀 Gerando...", spinner="dots"):
|
|
545
|
+
response = client.chat.completions.create(
|
|
546
|
+
model=MODEL,
|
|
547
|
+
response_format={"type": "json_object"},
|
|
548
|
+
messages=[
|
|
549
|
+
{"role": "system", "content": """
|
|
550
|
+
Você é um agente autônomo de programação.
|
|
551
|
+
REGRAS:
|
|
552
|
+
- Gere SOMENTE JSON válido
|
|
553
|
+
- Nunca explique
|
|
554
|
+
- Nunca use markdown
|
|
555
|
+
- Nunca escreva fora do JSON
|
|
556
|
+
- Gere arquivos completos
|
|
557
|
+
Formato obrigatório:
|
|
558
|
+
{
|
|
559
|
+
"files": [
|
|
560
|
+
{
|
|
561
|
+
"path": "arquivo.ext",
|
|
562
|
+
"content": "codigo"
|
|
563
|
+
}
|
|
564
|
+
]
|
|
565
|
+
}
|
|
566
|
+
""" + (f"\n\n---\n{make_claudio_context()}" if CLAUDIO_MD or CLAUDIO_SKILLS_TEXT or CLAUDIO_RULES else "")},
|
|
567
|
+
{"role": "user", "content": prompt}
|
|
568
|
+
],
|
|
569
|
+
temperature=TEMPERATURE,
|
|
570
|
+
max_tokens=MAX_TOKENS
|
|
571
|
+
)
|
|
572
|
+
content = response.choices[0].message.content.strip()
|
|
573
|
+
if is_invalid_response(content):
|
|
574
|
+
return None
|
|
575
|
+
return content
|
|
576
|
+
except Exception as e:
|
|
577
|
+
console.print(f"[red]❌ Erro OpenAI: {e}[/]")
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
def extract_json(text: str):
|
|
581
|
+
try:
|
|
582
|
+
data = json.loads(text)
|
|
583
|
+
if "files" not in data or not isinstance(data["files"], list):
|
|
584
|
+
return None
|
|
585
|
+
return data
|
|
586
|
+
except Exception as e:
|
|
587
|
+
console.print(f"[red]❌ JSON inválido: {e}[/]")
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
def create_project(user_prompt: str):
|
|
591
|
+
console.print()
|
|
592
|
+
console.print(Panel(
|
|
593
|
+
"[bold]Planejando projeto...[/]",
|
|
594
|
+
title="[bold yellow]🚀 Criar Projeto[/]",
|
|
595
|
+
border_style="yellow",
|
|
596
|
+
box=box.ROUNDED
|
|
597
|
+
))
|
|
598
|
+
|
|
599
|
+
ctx = make_claudio_context()
|
|
600
|
+
|
|
601
|
+
with Status("[bold yellow]📋 Gerando plano...", spinner="dots"):
|
|
602
|
+
try:
|
|
603
|
+
plan_resp = client.chat.completions.create(
|
|
604
|
+
model=MODEL,
|
|
605
|
+
response_format={"type": "json_object"},
|
|
606
|
+
messages=[
|
|
607
|
+
{"role": "system", "content": "Você é um arquiteto de projetos. Gere APENAS um plano em JSON, sem código." + ctx},
|
|
608
|
+
{"role": "user", "content": f"""Crie um plano para este pedido:
|
|
609
|
+
{user_prompt}
|
|
610
|
+
|
|
611
|
+
Se for web (Vite, React, Next, Nest, etc), inclua package.json, configs, index.html no plano.
|
|
612
|
+
|
|
613
|
+
Retorne JSON:
|
|
614
|
+
{{"plano": "descrição do que será criado",
|
|
615
|
+
"arquivos": [
|
|
616
|
+
{{"path": "caminho/arquivo.ext", "descricao": "o que este arquivo faz", "linguagem": "python/arduino/html/etc"}}
|
|
617
|
+
],
|
|
618
|
+
"total_estimado": 5}}"""}
|
|
619
|
+
],
|
|
620
|
+
temperature=0.3,
|
|
621
|
+
max_tokens=1000
|
|
622
|
+
)
|
|
623
|
+
plan_data = json.loads(plan_resp.choices[0].message.content)
|
|
624
|
+
plano_desc = plan_data.get("plano", "")
|
|
625
|
+
arquivos_planejados = plan_data.get("arquivos", [])
|
|
626
|
+
except Exception as e:
|
|
627
|
+
render_message("error", f"Erro ao gerar plano: {e}")
|
|
628
|
+
return
|
|
629
|
+
|
|
630
|
+
if not arquivos_planejados:
|
|
631
|
+
render_message("error", "Não foi possível gerar um plano")
|
|
632
|
+
return
|
|
633
|
+
|
|
634
|
+
render_message("assistant", f"**📋 Plano:**\n\n{plano_desc}\n\n**Arquivos:**")
|
|
635
|
+
for af in arquivos_planejados:
|
|
636
|
+
path = af.get("path", "?")
|
|
637
|
+
desc = af.get("descricao", "")
|
|
638
|
+
lang = af.get("linguagem", "")
|
|
639
|
+
render_message("info", f"📄 [bold]{path}[/] ([green]{lang}[/]) — {desc}")
|
|
640
|
+
|
|
641
|
+
confirm = Prompt.ask(
|
|
642
|
+
f"\n[bold yellow]Criar estes {len(arquivos_planejados)} arquivo(s)?[/]",
|
|
643
|
+
choices=["s", "n", "detalhar"],
|
|
644
|
+
default="s"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
if confirm == "n":
|
|
648
|
+
render_message("info", "Criação cancelada")
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
if confirm == "detalhar":
|
|
652
|
+
det_data = None
|
|
653
|
+
with Status("[bold yellow]🔍 Gerando detalhes...", spinner="dots"):
|
|
654
|
+
try:
|
|
655
|
+
det_resp = client.chat.completions.create(
|
|
656
|
+
model=MODEL,
|
|
657
|
+
response_format={"type": "json_object"},
|
|
658
|
+
messages=[
|
|
659
|
+
{"role": "system", "content": "Gere JSON com plano detalhado." + ctx},
|
|
660
|
+
{"role": "user", "content": f"""Detalhe o plano para: {user_prompt}
|
|
661
|
+
|
|
662
|
+
Retorne JSON:
|
|
663
|
+
{{"plano": "descrição detalhada da arquitetura",
|
|
664
|
+
"arquivos": [
|
|
665
|
+
{{"path": "...", "descricao": "...", "linguagem": "...", "dependencias": ["lib1"], "tamanho_estimado": "pequeno/médio/grande"}}
|
|
666
|
+
]}}"""}
|
|
667
|
+
],
|
|
668
|
+
temperature=0.3,
|
|
669
|
+
max_tokens=1500
|
|
670
|
+
)
|
|
671
|
+
det_data = json.loads(det_resp.choices[0].message.content)
|
|
672
|
+
except Exception as e:
|
|
673
|
+
render_message("error", f"Erro ao detalhar: {e}")
|
|
674
|
+
return
|
|
675
|
+
if det_data:
|
|
676
|
+
render_message("assistant", f"**📋 Plano Detalhado:**\n\n{det_data.get('plano', '')}")
|
|
677
|
+
for af in det_data.get("arquivos", arquivos_planejados):
|
|
678
|
+
path = af.get("path", "?")
|
|
679
|
+
desc = af.get("descricao", "")
|
|
680
|
+
lang = af.get("linguagem", "")
|
|
681
|
+
deps = af.get("dependencias", [])
|
|
682
|
+
size = af.get("tamanho_estimado", "")
|
|
683
|
+
deps_str = f" 📦 {', '.join(deps)}" if deps else ""
|
|
684
|
+
render_message("info", f"📄 [bold]{path}[/] ([green]{lang}[/], {size}) — {desc}{deps_str}")
|
|
685
|
+
if Prompt.ask("[bold yellow]Continuar com a criação?[/]", choices=["s", "n"], default="s") != "s":
|
|
686
|
+
render_message("info", "Criação cancelada")
|
|
687
|
+
return
|
|
688
|
+
|
|
689
|
+
render_message("info", "⏳ Gerando código dos arquivos...")
|
|
690
|
+
|
|
691
|
+
prompt = f"""
|
|
692
|
+
Gere um projeto completo e funcional para:
|
|
693
|
+
{user_prompt}
|
|
694
|
+
|
|
695
|
+
REGRAS:
|
|
696
|
+
- Inclua TODOS os arquivos necessários para o projeto rodar
|
|
697
|
+
- Para projetos web (Vite, React, Next.js, NestJS, etc), inclua:
|
|
698
|
+
- package.json com TODAS as dependências necessárias
|
|
699
|
+
- vite.config.js ou next.config.js ou similar
|
|
700
|
+
- index.html (se aplicável)
|
|
701
|
+
- tsconfig.json ou jsconfig.json (se aplicável)
|
|
702
|
+
- Todos os arquivos de src/ necessários
|
|
703
|
+
- Gere código completo e funcional
|
|
704
|
+
- Crie a estrutura de pastas adequada para o framework
|
|
705
|
+
"""
|
|
706
|
+
|
|
707
|
+
response = ask_ai(prompt)
|
|
708
|
+
if not response:
|
|
709
|
+
render_message("error", "Falha na resposta da IA")
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
data = extract_json(response)
|
|
713
|
+
if not data:
|
|
714
|
+
render_message("error", "JSON inválido")
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
files = data.get("files", [])
|
|
718
|
+
if not files:
|
|
719
|
+
render_message("error", "Nenhum arquivo retornado")
|
|
720
|
+
return
|
|
721
|
+
|
|
722
|
+
total = 0
|
|
723
|
+
for file_data in files:
|
|
724
|
+
path = file_data.get("path")
|
|
725
|
+
content = file_data.get("content")
|
|
726
|
+
if not path or not content:
|
|
727
|
+
continue
|
|
728
|
+
if write_file(path, content):
|
|
729
|
+
total += 1
|
|
730
|
+
|
|
731
|
+
render_message("info", f"✅ {total} arquivos criados")
|
|
732
|
+
|
|
733
|
+
has_package_json = any(f.get("path", "").endswith("package.json") for f in files)
|
|
734
|
+
|
|
735
|
+
if has_package_json:
|
|
736
|
+
render_message("info", "📦 package.json detectado")
|
|
737
|
+
if Prompt.ask("[bold yellow]Instalar dependências com npm?[/]", choices=["s", "n"], default="s") == "s":
|
|
738
|
+
with Status("[bold yellow]📦 Instalando dependências...", spinner="dots"):
|
|
739
|
+
r = subprocess.run("npm install", shell=True, capture_output=True, text=True, cwd=WORKSPACE)
|
|
740
|
+
if r.returncode == 0:
|
|
741
|
+
render_message("info", "✅ Dependências instaladas")
|
|
742
|
+
else:
|
|
743
|
+
render_message("error", f"npm install falhou: {r.stderr.strip()[:300]}")
|
|
744
|
+
|
|
745
|
+
if Prompt.ask("[bold yellow]Iniciar servidor de desenvolvimento?[/]", choices=["s", "n"], default="n") == "s":
|
|
746
|
+
render_message("info", "🚀 Abrindo servidor em novo terminal...")
|
|
747
|
+
if sys.platform == "win32":
|
|
748
|
+
subprocess.Popen("start cmd /k npm run dev", shell=True, cwd=WORKSPACE)
|
|
749
|
+
else:
|
|
750
|
+
subprocess.Popen("x-terminal-emulator -e npm run dev", shell=True, cwd=WORKSPACE)
|
|
751
|
+
render_message("info", "✅ Servidor iniciado em janela separada")
|
|
752
|
+
|
|
753
|
+
has_next = any("next" in f.get("path", "") or "next" in f.get("content", "").lower()[:200] for f in files)
|
|
754
|
+
has_vite = any("vite" in f.get("path", "") or "vite" in f.get("content", "").lower()[:200] for f in files)
|
|
755
|
+
|
|
756
|
+
if has_next:
|
|
757
|
+
render_message("info", "💡 Próximos passos: cd na pasta e `npm run dev`")
|
|
758
|
+
elif has_vite:
|
|
759
|
+
render_message("info", "💡 Próximos passos: `npm run dev` para iniciar o Vite")
|
|
760
|
+
else:
|
|
761
|
+
render_message("info", "💡 Próximos passos: veja o package.json para scripts disponíveis")
|
|
762
|
+
|
|
763
|
+
def analyze_code(file_path: str):
|
|
764
|
+
content = read_file(file_path)
|
|
765
|
+
if not content:
|
|
766
|
+
return "❌ Arquivo não encontrado"
|
|
767
|
+
|
|
768
|
+
extension = file_path.split(".")[-1] if "." in file_path else "txt"
|
|
769
|
+
|
|
770
|
+
ctx = make_claudio_context()
|
|
771
|
+
system_msg = {
|
|
772
|
+
"role": "system",
|
|
773
|
+
"content": "Você é um analisador de código. Responda SEMPRE em português. Analise o código abaixo.\n\n" + ctx
|
|
774
|
+
}
|
|
775
|
+
prompt = f"```{extension}\n{content[:4000]}\n```\n\nAnalise o código acima. Responda em português."
|
|
776
|
+
|
|
777
|
+
try:
|
|
778
|
+
with Status("[bold yellow]🔍 Analisando código...", spinner="dots"):
|
|
779
|
+
response = client.chat.completions.create(
|
|
780
|
+
model=MODEL,
|
|
781
|
+
messages=[system_msg, {"role": "user", "content": prompt}],
|
|
782
|
+
temperature=0.2,
|
|
783
|
+
max_tokens=2000
|
|
784
|
+
)
|
|
785
|
+
result = response.choices[0].message.content
|
|
786
|
+
render_message("assistant", result)
|
|
787
|
+
return result
|
|
788
|
+
except Exception as e:
|
|
789
|
+
error_msg = f"❌ {e}"
|
|
790
|
+
render_message("error", error_msg)
|
|
791
|
+
return error_msg
|
|
792
|
+
|
|
793
|
+
def analyze_project():
|
|
794
|
+
console.print()
|
|
795
|
+
render_message("info", "Analisando projeto completo...")
|
|
796
|
+
context = get_project_context()
|
|
797
|
+
|
|
798
|
+
ctx = make_claudio_context()
|
|
799
|
+
system_msg = {
|
|
800
|
+
"role": "system",
|
|
801
|
+
"content": "Você é um analisador de código. Responda SEMPRE em português. Analise o projeto abaixo.\n\n" + ctx
|
|
802
|
+
}
|
|
803
|
+
prompt = f"""```\n{context}\n```
|
|
804
|
+
|
|
805
|
+
Analise este projeto. Responda em português:
|
|
806
|
+
- arquitetura
|
|
807
|
+
- bugs e problemas de segurança
|
|
808
|
+
- code smells
|
|
809
|
+
- arquivos importantes
|
|
810
|
+
- sugestões de melhoria
|
|
811
|
+
"""
|
|
812
|
+
|
|
813
|
+
try:
|
|
814
|
+
with Status("[bold yellow]🔍 Analisando projeto...", spinner="dots"):
|
|
815
|
+
response = client.chat.completions.create(
|
|
816
|
+
model=MODEL,
|
|
817
|
+
messages=[system_msg, {"role": "user", "content": prompt}],
|
|
818
|
+
temperature=0.2,
|
|
819
|
+
max_tokens=4000
|
|
820
|
+
)
|
|
821
|
+
result = response.choices[0].message.content
|
|
822
|
+
render_message("assistant", result)
|
|
823
|
+
except Exception as e:
|
|
824
|
+
render_message("error", f"{e}")
|
|
825
|
+
|
|
826
|
+
def debug_code(file_path: str, error: str):
|
|
827
|
+
content = read_file(file_path)
|
|
828
|
+
if not content:
|
|
829
|
+
return "❌ Arquivo não encontrado"
|
|
830
|
+
|
|
831
|
+
extension = file_path.split(".")[-1] if "." in file_path else "txt"
|
|
832
|
+
prompt = f"Corrija este código.\n\nErro:\n{error}\n\nArquivo:\n{file_path}\n\n```{extension}\n{content[:4000]}\n```"
|
|
833
|
+
|
|
834
|
+
try:
|
|
835
|
+
with Status("[bold yellow]🐛 Debugando...", spinner="dots"):
|
|
836
|
+
response = client.chat.completions.create(
|
|
837
|
+
model=MODEL,
|
|
838
|
+
messages=[{"role": "user", "content": prompt}],
|
|
839
|
+
temperature=0.1,
|
|
840
|
+
max_tokens=3000
|
|
841
|
+
)
|
|
842
|
+
result = response.choices[0].message.content
|
|
843
|
+
render_message("assistant", result)
|
|
844
|
+
return result
|
|
845
|
+
except Exception as e:
|
|
846
|
+
error_msg = f"❌ {e}"
|
|
847
|
+
render_message("error", error_msg)
|
|
848
|
+
return error_msg
|
|
849
|
+
|
|
850
|
+
def list_directory(path: str = "."):
|
|
851
|
+
try:
|
|
852
|
+
target = WORKSPACE / path
|
|
853
|
+
if not target.exists():
|
|
854
|
+
render_message("error", f"Pasta não encontrada: {path}")
|
|
855
|
+
return None
|
|
856
|
+
if not target.is_dir():
|
|
857
|
+
return None
|
|
858
|
+
|
|
859
|
+
tree = []
|
|
860
|
+
for root, dirs, files in os.walk(target):
|
|
861
|
+
dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
|
|
862
|
+
rel = Path(root).relative_to(target)
|
|
863
|
+
level = len(rel.parts)
|
|
864
|
+
indent = " " * 4 * level
|
|
865
|
+
if level == 0:
|
|
866
|
+
tree.append(f"{target.name}/")
|
|
867
|
+
else:
|
|
868
|
+
tree.append(f"{indent}{rel.name}/")
|
|
869
|
+
subindent = " " * 4 * (level + 1)
|
|
870
|
+
for f in sorted(files):
|
|
871
|
+
tree.append(f"{subindent}{f}")
|
|
872
|
+
|
|
873
|
+
result = "\n".join(tree)
|
|
874
|
+
render_message("code", result)
|
|
875
|
+
return result
|
|
876
|
+
except Exception as e:
|
|
877
|
+
render_message("error", f"{e}")
|
|
878
|
+
return None
|
|
879
|
+
|
|
880
|
+
def analyze_folder(folder_path: str):
|
|
881
|
+
target = Path(folder_path)
|
|
882
|
+
if not target.is_absolute():
|
|
883
|
+
target = WORKSPACE / folder_path
|
|
884
|
+
|
|
885
|
+
if not target.exists():
|
|
886
|
+
render_message("error", f"Pasta não encontrada: {target}")
|
|
887
|
+
return
|
|
888
|
+
if not target.is_dir():
|
|
889
|
+
analyze_code(str(target))
|
|
890
|
+
return
|
|
891
|
+
|
|
892
|
+
render_message("info", f"[bold]> Pasta alvo:[/] {target}")
|
|
893
|
+
render_message("info", f"[bold]> Lendo arquivos...[/]")
|
|
894
|
+
|
|
895
|
+
files_content = []
|
|
896
|
+
total_chars = 0
|
|
897
|
+
total_read = 0
|
|
898
|
+
skipped = 0
|
|
899
|
+
for root, dirs, files in os.walk(target):
|
|
900
|
+
dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
|
|
901
|
+
for file in sorted(files):
|
|
902
|
+
fpath = Path(root) / file
|
|
903
|
+
if fpath.suffix not in SUPPORTED_EXTENSIONS:
|
|
904
|
+
continue
|
|
905
|
+
try:
|
|
906
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
907
|
+
rel = fpath.relative_to(WORKSPACE) if target != WORKSPACE else fpath.relative_to(target)
|
|
908
|
+
if len(content) > MAX_FILE_PREVIEW:
|
|
909
|
+
content = content[:MAX_FILE_PREVIEW] + "\n// ... [TRUNCADO]"
|
|
910
|
+
if total_chars + len(content) > MAX_CONTEXT_CHARS:
|
|
911
|
+
skipped += 1
|
|
912
|
+
continue
|
|
913
|
+
files_content.append(f"// {rel}\n{content}")
|
|
914
|
+
total_chars += len(content)
|
|
915
|
+
total_read += 1
|
|
916
|
+
except:
|
|
917
|
+
pass
|
|
918
|
+
|
|
919
|
+
if not files_content:
|
|
920
|
+
render_message("error", "Nenhum arquivo suportado encontrado nesta pasta")
|
|
921
|
+
return
|
|
922
|
+
|
|
923
|
+
msg = f"[bold]> {total_read} arquivos lidos[/]"
|
|
924
|
+
if skipped:
|
|
925
|
+
msg += f" ([yellow]{skipped} ignorados por limite de tamanho[/])"
|
|
926
|
+
render_message("info", msg)
|
|
927
|
+
|
|
928
|
+
code_block = "\n\n".join(files_content)
|
|
929
|
+
|
|
930
|
+
ctx = make_claudio_context()
|
|
931
|
+
system_msg = {
|
|
932
|
+
"role": "system",
|
|
933
|
+
"content": "Você é um analisador de código. Responda SEMPRE em português. Analise o código abaixo.\n\n" + ctx
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
prompt = f"""{code_block}
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
940
|
+
Revise cada trecho acima. Para cada um:
|
|
941
|
+
- o que faz
|
|
942
|
+
- bugs, problemas de segurança, code smells
|
|
943
|
+
- melhorias
|
|
944
|
+
|
|
945
|
+
Depois, análise geral:
|
|
946
|
+
- propósito do conjunto
|
|
947
|
+
- arquitetura
|
|
948
|
+
- pontos fortes/fracos
|
|
949
|
+
- sugestões de refatoração
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
try:
|
|
953
|
+
with Status("[bold yellow]🔍 Analisando pasta...", spinner="dots"):
|
|
954
|
+
response = client.chat.completions.create(
|
|
955
|
+
model=MODEL,
|
|
956
|
+
messages=[system_msg, {"role": "user", "content": prompt}],
|
|
957
|
+
temperature=0.2,
|
|
958
|
+
max_tokens=6000
|
|
959
|
+
)
|
|
960
|
+
result = response.choices[0].message.content
|
|
961
|
+
render_message("assistant", result)
|
|
962
|
+
except Exception as e:
|
|
963
|
+
render_message("error", f"{e}")
|
|
964
|
+
|
|
965
|
+
def read_file_content(file_path: str):
|
|
966
|
+
content = read_file(file_path)
|
|
967
|
+
if content is None:
|
|
968
|
+
render_message("error", f"Arquivo não encontrado: {file_path}")
|
|
969
|
+
return
|
|
970
|
+
syntax = Syntax(content, file_path.split(".")[-1] if "." in file_path else "txt",
|
|
971
|
+
theme="monokai", line_numbers=True)
|
|
972
|
+
console.print(Panel(
|
|
973
|
+
syntax,
|
|
974
|
+
title=f"[bold blue]{file_path}[/]",
|
|
975
|
+
border_style="blue",
|
|
976
|
+
box=box.ROUNDED,
|
|
977
|
+
padding=(1, 2)
|
|
978
|
+
))
|
|
979
|
+
|
|
980
|
+
def run_terminal(command: str):
|
|
981
|
+
try:
|
|
982
|
+
with Status("[bold yellow]⚡ Executando...", spinner="dots"):
|
|
983
|
+
result = subprocess.run(
|
|
984
|
+
command, shell=True, capture_output=True, text=True, cwd=WORKSPACE
|
|
985
|
+
)
|
|
986
|
+
if result.stdout:
|
|
987
|
+
console.print(Panel(
|
|
988
|
+
result.stdout.strip() or "(sem output)",
|
|
989
|
+
title="[bold green]Output[/]",
|
|
990
|
+
border_style="green",
|
|
991
|
+
box=box.ROUNDED,
|
|
992
|
+
padding=(1, 2)
|
|
993
|
+
))
|
|
994
|
+
if result.stderr:
|
|
995
|
+
console.print(Panel(
|
|
996
|
+
result.stderr.strip(),
|
|
997
|
+
title="[bold red]Erro[/]",
|
|
998
|
+
border_style="red",
|
|
999
|
+
box=box.ROUNDED,
|
|
1000
|
+
padding=(1, 2)
|
|
1001
|
+
))
|
|
1002
|
+
except Exception as e:
|
|
1003
|
+
render_message("error", f"{e}")
|
|
1004
|
+
|
|
1005
|
+
def list_skills():
|
|
1006
|
+
if not CLAUDIO_SKILLS:
|
|
1007
|
+
render_message("info", "Nenhuma skill encontrada em .claudio/skills/")
|
|
1008
|
+
return
|
|
1009
|
+
table = Table(title="Skills Disponíveis", box=box.ROUNDED, border_style="green")
|
|
1010
|
+
table.add_column("Skill", style="cyan", no_wrap=True)
|
|
1011
|
+
table.add_column("Arquivo", style="white")
|
|
1012
|
+
for s in CLAUDIO_SKILLS:
|
|
1013
|
+
table.add_row(s["name"], str(s["path"].relative_to(WORKSPACE)))
|
|
1014
|
+
console.print(table)
|
|
1015
|
+
render_message("info", "Use: executar skill <nome>")
|
|
1016
|
+
|
|
1017
|
+
def execute_skill(skill_name: str, user_message: str = ""):
|
|
1018
|
+
skill = None
|
|
1019
|
+
skill_lower = skill_name.lower()
|
|
1020
|
+
skill_stem = Path(skill_lower).stem
|
|
1021
|
+
for s in CLAUDIO_SKILLS:
|
|
1022
|
+
sname = s["name"].lower()
|
|
1023
|
+
if sname == skill_lower or Path(sname).stem == skill_stem:
|
|
1024
|
+
skill = s
|
|
1025
|
+
break
|
|
1026
|
+
if not skill:
|
|
1027
|
+
render_message("error", f"Skill '{skill_name}' não encontrada")
|
|
1028
|
+
return
|
|
1029
|
+
|
|
1030
|
+
render_message("info", f"Usando skill como prompt: [bold]{skill['name']}[/]")
|
|
1031
|
+
full_prompt = f"""{skill['content']}
|
|
1032
|
+
|
|
1033
|
+
{user_message or "Crie um site completo baseado nas instruções acima."}"""
|
|
1034
|
+
create_project(full_prompt)
|
|
1035
|
+
|
|
1036
|
+
def edit_file(file_path: str, instruction: str):
|
|
1037
|
+
content = read_file(file_path)
|
|
1038
|
+
if content is None:
|
|
1039
|
+
render_message("error", f"Arquivo não encontrado: {file_path}")
|
|
1040
|
+
return
|
|
1041
|
+
ext = Path(file_path).suffix or ".txt"
|
|
1042
|
+
ctx = make_claudio_context()
|
|
1043
|
+
prompt = f"""Edite o arquivo abaixo seguindo a instrução.
|
|
1044
|
+
|
|
1045
|
+
ARQUIVO: {file_path}
|
|
1046
|
+
INSTRUÇÃO: {instruction}
|
|
1047
|
+
|
|
1048
|
+
```{ext}
|
|
1049
|
+
{content[:MAX_FILE_PREVIEW]}
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
REGRAS:
|
|
1053
|
+
- Retorne APENAS o código modificado completo
|
|
1054
|
+
- Não explique as mudanças
|
|
1055
|
+
- Não use markdown"""
|
|
1056
|
+
with Status("[bold yellow]✏️ Editando arquivo...", spinner="dots"):
|
|
1057
|
+
try:
|
|
1058
|
+
resp = client.chat.completions.create(
|
|
1059
|
+
model=MODEL,
|
|
1060
|
+
messages=[
|
|
1061
|
+
{"role": "system", "content": "Você é um editor de código. Retorne APENAS o código modificado." + ctx},
|
|
1062
|
+
{"role": "user", "content": prompt}
|
|
1063
|
+
],
|
|
1064
|
+
temperature=0.2,
|
|
1065
|
+
max_tokens=MAX_TOKENS
|
|
1066
|
+
)
|
|
1067
|
+
new_content = resp.choices[0].message.content
|
|
1068
|
+
if not new_content or is_invalid_response(new_content):
|
|
1069
|
+
render_message("error", "Resposta inválida da IA")
|
|
1070
|
+
return
|
|
1071
|
+
new_content = re.sub(r'^```\w*\n?', '', new_content)
|
|
1072
|
+
new_content = re.sub(r'\n?```$', '', new_content)
|
|
1073
|
+
full_path = WORKSPACE / file_path if not Path(file_path).is_absolute() else Path(file_path)
|
|
1074
|
+
render_message("info", f"Preview das alterações em {file_path}:")
|
|
1075
|
+
console.print(Syntax(new_content[:2500], ext.lstrip('.'), theme="monokai", line_numbers=True))
|
|
1076
|
+
if Prompt.ask("[bold yellow]Aplicar alterações?[/]", choices=["s", "n"], default="s") == "s":
|
|
1077
|
+
full_path.write_text(new_content, encoding="utf-8")
|
|
1078
|
+
render_message("info", f"✅ {file_path} atualizado")
|
|
1079
|
+
else:
|
|
1080
|
+
render_message("info", "Alterações canceladas")
|
|
1081
|
+
except Exception as e:
|
|
1082
|
+
render_message("error", f"Erro ao editar: {e}")
|
|
1083
|
+
|
|
1084
|
+
def refactor_code(file_path: str):
|
|
1085
|
+
content = read_file(file_path)
|
|
1086
|
+
if content is None:
|
|
1087
|
+
render_message("error", f"Arquivo não encontrado: {file_path}")
|
|
1088
|
+
return
|
|
1089
|
+
ext = Path(file_path).suffix or ".txt"
|
|
1090
|
+
ctx = make_claudio_context()
|
|
1091
|
+
with Status("[bold yellow]🔍 Analisando para refatorar...", spinner="dots"):
|
|
1092
|
+
try:
|
|
1093
|
+
analysis = client.chat.completions.create(
|
|
1094
|
+
model=MODEL,
|
|
1095
|
+
messages=[
|
|
1096
|
+
{"role": "system", "content": "Você é um analisador de código. Seja técnico e específico." + ctx},
|
|
1097
|
+
{"role": "user", "content": f"Analise este código para refatoração:\n\n```{ext}\n{content[:MAX_FILE_PREVIEW]}\n```\n\nIdentifique: code smells, violações de boas práticas, oportunidades de melhoria, complexidade desnecessária."}
|
|
1098
|
+
],
|
|
1099
|
+
temperature=0.2,
|
|
1100
|
+
max_tokens=2000
|
|
1101
|
+
).choices[0].message.content
|
|
1102
|
+
render_message("assistant", f"**📋 Análise para refatoração:**\n\n{analysis}")
|
|
1103
|
+
if Prompt.ask("[bold yellow]Gerar versão refatorada?[/]", choices=["s", "n"], default="s") != "s":
|
|
1104
|
+
return
|
|
1105
|
+
with Status("[bold yellow]🔄 Refatorando...", spinner="dots"):
|
|
1106
|
+
refactored = client.chat.completions.create(
|
|
1107
|
+
model=MODEL,
|
|
1108
|
+
messages=[
|
|
1109
|
+
{"role": "system", "content": "Refatore o código. Retorne APENAS o código refatorado completo. Mantenha a mesma funcionalidade." + ctx},
|
|
1110
|
+
{"role": "user", "content": f"Refatore:\n\n```{ext}\n{content[:MAX_FILE_PREVIEW]}\n```"}
|
|
1111
|
+
],
|
|
1112
|
+
temperature=0.2,
|
|
1113
|
+
max_tokens=MAX_TOKENS
|
|
1114
|
+
).choices[0].message.content
|
|
1115
|
+
new_content = re.sub(r'^```\w*\n?', '', refactored)
|
|
1116
|
+
new_content = re.sub(r'\n?```$', '', new_content)
|
|
1117
|
+
full_path = WORKSPACE / file_path if not Path(file_path).is_absolute() else Path(file_path)
|
|
1118
|
+
render_message("info", f"Preview da versão refatorada:")
|
|
1119
|
+
console.print(Syntax(new_content[:2500], ext.lstrip('.'), theme="monokai", line_numbers=True))
|
|
1120
|
+
if Prompt.ask("[bold yellow]Aplicar refatoração?[/]", choices=["s", "n"], default="s") == "s":
|
|
1121
|
+
full_path.write_text(new_content, encoding="utf-8")
|
|
1122
|
+
render_message("info", f"✅ {file_path} refatorado")
|
|
1123
|
+
else:
|
|
1124
|
+
render_message("info", "Refatoração cancelada")
|
|
1125
|
+
except Exception as e:
|
|
1126
|
+
render_message("error", f"Erro na refatoração: {e}")
|
|
1127
|
+
|
|
1128
|
+
def web_search(query: str):
|
|
1129
|
+
with Status(f"[bold yellow]🌐 Buscando: {query}...", spinner="dots"):
|
|
1130
|
+
try:
|
|
1131
|
+
url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
|
|
1132
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"})
|
|
1133
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
1134
|
+
html = resp.read().decode("utf-8", errors="ignore")
|
|
1135
|
+
results = []
|
|
1136
|
+
for m in re.finditer(r'<a[^>]*class="result__a"[^>]*>(.*?)</a>.*?class="result__snippet"[^>]*>(.*?)</a>', html, re.DOTALL):
|
|
1137
|
+
title = re.sub(r'<[^>]+>', '', m.group(1)).strip()
|
|
1138
|
+
snippet = re.sub(r'<[^>]+>', '', m.group(2)).strip()
|
|
1139
|
+
results.append(f"• {title}\n {snippet}")
|
|
1140
|
+
text = "\n\n".join(results[:10]) if results else "Nenhum resultado encontrado."
|
|
1141
|
+
render_message("assistant", f"**🌐 Resultados para:** {query}\n\n{text}")
|
|
1142
|
+
except Exception as e:
|
|
1143
|
+
render_message("info", f"Não foi possível buscar online. Usando conhecimento da IA...")
|
|
1144
|
+
normal_chat(f"Pesquise e responda sobre: {query}")
|
|
1145
|
+
|
|
1146
|
+
def fetch_url(url: str):
|
|
1147
|
+
with Status(f"[bold yellow]🌐 Acessando: {url}...", spinner="dots"):
|
|
1148
|
+
try:
|
|
1149
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
1150
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
1151
|
+
content = resp.read().decode("utf-8", errors="ignore")
|
|
1152
|
+
text = re.sub(r'<[^>]+>', ' ', content)
|
|
1153
|
+
text = re.sub(r'\s+', ' ', text).strip()[:8000]
|
|
1154
|
+
render_message("assistant", f"**📄 Conteúdo de:** {url}\n\n{text}")
|
|
1155
|
+
except Exception as e:
|
|
1156
|
+
render_message("error", f"Erro ao acessar URL: {e}")
|
|
1157
|
+
|
|
1158
|
+
def git_command(args: str):
|
|
1159
|
+
with Status(f"[bold yellow]🔧 git {args}...", spinner="dots"):
|
|
1160
|
+
try:
|
|
1161
|
+
result = subprocess.run(f"git {args}", shell=True, capture_output=True, text=True, cwd=WORKSPACE)
|
|
1162
|
+
if result.returncode != 0:
|
|
1163
|
+
render_message("error", result.stderr.strip() or f"git falhou (código {result.returncode})")
|
|
1164
|
+
return
|
|
1165
|
+
output = result.stdout.strip() or result.stderr.strip() or "(sem output)"
|
|
1166
|
+
render_message("code", output[:5000])
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
render_message("error", f"Erro git: {e}")
|
|
1169
|
+
|
|
1170
|
+
def reconfigurar():
|
|
1171
|
+
global API_KEY, MODEL, client
|
|
1172
|
+
console.print(Panel(
|
|
1173
|
+
"[bold]Reconfigurando claudio...[/]",
|
|
1174
|
+
title="[bold cyan]⚙️ Configuração[/]",
|
|
1175
|
+
border_style="cyan",
|
|
1176
|
+
box=box.ROUNDED
|
|
1177
|
+
))
|
|
1178
|
+
|
|
1179
|
+
old_key = API_KEY
|
|
1180
|
+
masked = old_key[:8] + "..." + old_key[-4:] if len(old_key) > 12 else "***"
|
|
1181
|
+
console.print(f"Chave atual: [green]{masked}[/]")
|
|
1182
|
+
if Prompt.ask("[bold yellow]Alterar chave?[/]", choices=["s", "n"], default="n") == "s":
|
|
1183
|
+
new_key = Prompt.ask("[bold yellow]🔑 Nova API Key[/]", password=True)
|
|
1184
|
+
while not new_key or not new_key.startswith("sk-"):
|
|
1185
|
+
console.print("[red]❌ Chave inválida. Deve começar com 'sk-'[/]")
|
|
1186
|
+
new_key = Prompt.ask("[bold yellow]🔑 Nova API Key[/]", password=True)
|
|
1187
|
+
API_KEY = new_key
|
|
1188
|
+
save_config({"api_key": new_key})
|
|
1189
|
+
client = OpenAI(api_key=API_KEY, timeout=120)
|
|
1190
|
+
console.print("[green]✅ Chave atualizada[/]")
|
|
1191
|
+
|
|
1192
|
+
console.print(f"\nModelo atual: [green]{MODEL}[/]")
|
|
1193
|
+
if Prompt.ask("[bold yellow]Alterar modelo?[/]", choices=["s", "n"], default="n") == "s":
|
|
1194
|
+
for i, m in enumerate(AVAILABLE_MODELS, 1):
|
|
1195
|
+
console.print(f" [cyan]{i}.[/] {m}")
|
|
1196
|
+
model_idx = Prompt.ask("[bold yellow]🎯 Escolha o modelo[/]", choices=[str(i) for i in range(1, len(AVAILABLE_MODELS)+1)], default="1")
|
|
1197
|
+
MODEL = AVAILABLE_MODELS[int(model_idx) - 1]
|
|
1198
|
+
save_config({"model": MODEL})
|
|
1199
|
+
console.print(f"[green]✅ Modelo alterado para: {MODEL}[/]")
|
|
1200
|
+
|
|
1201
|
+
render_message("info", "⚙️ Configuração concluída")
|
|
1202
|
+
|
|
1203
|
+
def switch_model(model_name: str):
|
|
1204
|
+
global MODEL
|
|
1205
|
+
if not model_name:
|
|
1206
|
+
render_message("info", f"Modelo atual: {MODEL}\nDisponíveis: {', '.join(AVAILABLE_MODELS)}")
|
|
1207
|
+
return
|
|
1208
|
+
lower = model_name.lower()
|
|
1209
|
+
matches = [m for m in AVAILABLE_MODELS if lower in m.lower()]
|
|
1210
|
+
if len(matches) == 0:
|
|
1211
|
+
render_message("error", f"Modelo '{model_name}' não encontrado. Disponíveis: {', '.join(AVAILABLE_MODELS)}")
|
|
1212
|
+
elif len(matches) == 1:
|
|
1213
|
+
MODEL = matches[0]
|
|
1214
|
+
save_config({"model": MODEL})
|
|
1215
|
+
render_message("info", f"🧠 Modelo alterado para: {MODEL}")
|
|
1216
|
+
else:
|
|
1217
|
+
render_message("info", f"Múltiplos correspondem: {', '.join(matches)}")
|
|
1218
|
+
|
|
1219
|
+
def auto_install_deps():
|
|
1220
|
+
with Status("[bold yellow]🔍 Examinando dependências...", spinner="dots"):
|
|
1221
|
+
try:
|
|
1222
|
+
imports = set()
|
|
1223
|
+
for root, dirs, files in os.walk(WORKSPACE):
|
|
1224
|
+
dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
|
|
1225
|
+
for f in files:
|
|
1226
|
+
if f.endswith(".py"):
|
|
1227
|
+
content = Path(root, f).read_text(encoding="utf-8", errors="ignore")
|
|
1228
|
+
for m in re.finditer(r'^(?:import|from)\s+(\w+)', content, re.MULTILINE):
|
|
1229
|
+
imports.add(m.group(1))
|
|
1230
|
+
stdlib = {"os", "sys", "json", "re", "subprocess", "io", "pathlib", "datetime",
|
|
1231
|
+
"math", "random", "collections", "functools", "itertools", "typing",
|
|
1232
|
+
"abc", "copy", "enum", "hashlib", "base64", "textwrap", "uuid",
|
|
1233
|
+
"urllib", "http", "socket", "threading", "multiprocessing", "xml",
|
|
1234
|
+
"csv", "sqlite3", "html", "inspect", "logging", "pprint", "queue",
|
|
1235
|
+
"shutil", "string", "struct", "tempfile", "time", "traceback",
|
|
1236
|
+
"weakref", "zipfile"}
|
|
1237
|
+
third_party = {i for i in imports if i not in stdlib and not i.startswith("_")}
|
|
1238
|
+
if not third_party:
|
|
1239
|
+
render_message("info", "Nenhuma dependência externa detectada")
|
|
1240
|
+
return
|
|
1241
|
+
render_message("info", f"Dependências detectadas: {', '.join(sorted(third_party))}")
|
|
1242
|
+
if Prompt.ask("[bold yellow]Instalar todas?[/]", choices=["s", "n"], default="s") != "s":
|
|
1243
|
+
return
|
|
1244
|
+
for pkg in sorted(third_party):
|
|
1245
|
+
render_message("info", f"📦 Instalando {pkg}...")
|
|
1246
|
+
r = subprocess.run(f"pip install {pkg}", shell=True, capture_output=True, text=True)
|
|
1247
|
+
if r.returncode == 0:
|
|
1248
|
+
render_message("info", f"✅ {pkg} instalado")
|
|
1249
|
+
else:
|
|
1250
|
+
render_message("error", f"❌ {pkg}: {r.stderr.strip()[:200]}")
|
|
1251
|
+
except Exception as e:
|
|
1252
|
+
render_message("error", f"Erro: {e}")
|
|
1253
|
+
|
|
1254
|
+
def autonomous_mode(task: str):
|
|
1255
|
+
render_message("info", f"🎯 Missão autônoma: {task}")
|
|
1256
|
+
ctx = make_claudio_context()
|
|
1257
|
+
structure = get_project_structure()[:2000]
|
|
1258
|
+
with Status("[bold yellow]🧠 Planejando missão...", spinner="dots"):
|
|
1259
|
+
try:
|
|
1260
|
+
plan_resp = client.chat.completions.create(
|
|
1261
|
+
model=MODEL,
|
|
1262
|
+
response_format={"type": "json_object"},
|
|
1263
|
+
messages=[
|
|
1264
|
+
{"role": "system", "content": "Você é um planejador de missões. Gere JSON com planos detalhados." + ctx},
|
|
1265
|
+
{"role": "user", "content": f"""Crie um plano para executar esta missão:
|
|
1266
|
+
|
|
1267
|
+
MISSÃO: {task}
|
|
1268
|
+
|
|
1269
|
+
Workspace:
|
|
1270
|
+
{structure}
|
|
1271
|
+
|
|
1272
|
+
Retorne JSON:
|
|
1273
|
+
{{"passos": [
|
|
1274
|
+
{{"tipo": "ler|escrever|executar|analisar|perguntar", "alvo": "...", "descricao": "..."}}
|
|
1275
|
+
]}}"""}
|
|
1276
|
+
],
|
|
1277
|
+
temperature=0.2,
|
|
1278
|
+
max_tokens=2000
|
|
1279
|
+
)
|
|
1280
|
+
plan_data = json.loads(plan_resp.choices[0].message.content)
|
|
1281
|
+
passos = plan_data.get("passos", [])
|
|
1282
|
+
if not passos:
|
|
1283
|
+
render_message("error", "Não foi possível gerar um plano")
|
|
1284
|
+
return
|
|
1285
|
+
render_message("info", f"📋 Plano: {len(passos)} passos")
|
|
1286
|
+
results = []
|
|
1287
|
+
for i, passo in enumerate(passos, 1):
|
|
1288
|
+
tipo = passo.get("tipo", "")
|
|
1289
|
+
alvo = passo.get("alvo", "")
|
|
1290
|
+
desc = passo.get("descricao", "")
|
|
1291
|
+
render_message("info", f"[bold][{i}/{len(passos)}][/] {desc}")
|
|
1292
|
+
if tipo == "ler":
|
|
1293
|
+
c = read_file(alvo)
|
|
1294
|
+
results.append(f"// {alvo}\n{c[:2000] if c else 'não encontrado'}" if c else f"// {alvo}: não encontrado")
|
|
1295
|
+
elif tipo == "escrever":
|
|
1296
|
+
gen = client.chat.completions.create(
|
|
1297
|
+
model=MODEL,
|
|
1298
|
+
messages=[
|
|
1299
|
+
{"role": "system", "content": f"Gere código para {alvo} baseado na missão: {task}"},
|
|
1300
|
+
{"role": "user", "content": f"Contexto:\n{chr(10).join(results[-3:])}\n\nGere o conteúdo completo para {alvo}"}
|
|
1301
|
+
],
|
|
1302
|
+
temperature=0.2,
|
|
1303
|
+
max_tokens=4000
|
|
1304
|
+
).choices[0].message.content
|
|
1305
|
+
write_file(alvo, gen)
|
|
1306
|
+
results.append(f"// {alvo}: criado")
|
|
1307
|
+
elif tipo == "executar":
|
|
1308
|
+
r = subprocess.run(alvo, shell=True, capture_output=True, text=True, cwd=WORKSPACE)
|
|
1309
|
+
out = (r.stdout or r.stderr or "")[-1000:]
|
|
1310
|
+
results.append(f"$ {alvo}\n{out}")
|
|
1311
|
+
render_message("code", out[:2000])
|
|
1312
|
+
elif tipo == "analisar":
|
|
1313
|
+
c = read_file(alvo)
|
|
1314
|
+
if c:
|
|
1315
|
+
a = client.chat.completions.create(
|
|
1316
|
+
model=MODEL,
|
|
1317
|
+
messages=[{"role": "system", "content": "Analise o código objetivamente."}, {"role": "user", "content": f"```\n{c[:3000]}\n```"}],
|
|
1318
|
+
temperature=0.2, max_tokens=1000
|
|
1319
|
+
).choices[0].message.content
|
|
1320
|
+
results.append(f"// análise {alvo}:\n{a}")
|
|
1321
|
+
render_message("assistant", a[:2000])
|
|
1322
|
+
elif tipo == "perguntar":
|
|
1323
|
+
a = client.chat.completions.create(
|
|
1324
|
+
model=MODEL,
|
|
1325
|
+
messages=[{"role": "system", "content": "Você é um assistente de programação." + ctx}, {"role": "user", "content": f"Missão: {task}\n\n{alvo}"}],
|
|
1326
|
+
temperature=0.3, max_tokens=1000
|
|
1327
|
+
).choices[0].message.content
|
|
1328
|
+
results.append(f"// consulta:\n{a}")
|
|
1329
|
+
render_message("info", "🏁 Missão concluída!")
|
|
1330
|
+
summary = client.chat.completions.create(
|
|
1331
|
+
model=MODEL,
|
|
1332
|
+
messages=[
|
|
1333
|
+
{"role": "system", "content": "Resuma em português o resultado da missão."},
|
|
1334
|
+
{"role": "user", "content": f"MISSÃO: {task}\n\nRESULTADOS:\n{chr(10).join(results[-8:])}"}
|
|
1335
|
+
],
|
|
1336
|
+
temperature=0.3, max_tokens=1000
|
|
1337
|
+
).choices[0].message.content
|
|
1338
|
+
render_message("assistant", f"**📊 Resumo da Missão**\n\n{summary}")
|
|
1339
|
+
except json.JSONDecodeError:
|
|
1340
|
+
render_message("error", "Erro ao interpretar plano. Executando como chat...")
|
|
1341
|
+
normal_chat(f"Crie um plano e execute a seguinte missão passo a passo: {task}")
|
|
1342
|
+
except Exception as e:
|
|
1343
|
+
render_message("error", f"Erro na missão: {e}")
|
|
1344
|
+
|
|
1345
|
+
def show_help():
|
|
1346
|
+
help_table = Table(title="Comandos Disponíveis", box=box.ROUNDED, border_style="cyan")
|
|
1347
|
+
help_table.add_column("Comando", style="cyan", no_wrap=True)
|
|
1348
|
+
help_table.add_column("Descrição", style="white")
|
|
1349
|
+
help_table.add_row("analisar projeto", "Analisa todo o projeto")
|
|
1350
|
+
help_table.add_row("analisar <arquivo/pasta>", "Analisa um alvo específico")
|
|
1351
|
+
help_table.add_row("listar/ls <caminho>", "Lista arquivos de uma pasta")
|
|
1352
|
+
help_table.add_row("ler <arquivo>", "Mostra o conteúdo de um arquivo")
|
|
1353
|
+
help_table.add_row("editar <arquivo> <desc>", "Edita um arquivo via IA")
|
|
1354
|
+
help_table.add_row("refatorar <arquivo>", "Refatora código automaticamente")
|
|
1355
|
+
help_table.add_row("debug <arquivo> <erro>", "Debuga um arquivo com erro")
|
|
1356
|
+
help_table.add_row("executar <comando>", "Executa comando no terminal")
|
|
1357
|
+
help_table.add_row("git <args>", "Executa comandos git")
|
|
1358
|
+
help_table.add_row("buscar <query>", "Busca na internet")
|
|
1359
|
+
help_table.add_row("site <url>", "Acessa conteúdo de uma URL")
|
|
1360
|
+
help_table.add_row("executar skill <nome>", "Executa uma skill")
|
|
1361
|
+
help_table.add_row("skills", "Lista skills disponíveis")
|
|
1362
|
+
help_table.add_row("crie/criar/gere/faça <desc>", "Gera um projeto completo")
|
|
1363
|
+
help_table.add_row("autonomo/missao <desc>", "Modo autônomo multi-passo")
|
|
1364
|
+
help_table.add_row("modelo <nome>", "Troca o modelo GPT")
|
|
1365
|
+
help_table.add_row("config", "Altera chave API e modelo")
|
|
1366
|
+
help_table.add_row("instalar/deps", "Auto-instala dependências")
|
|
1367
|
+
help_table.add_row("salvar", "Salva o histórico da sessão")
|
|
1368
|
+
help_table.add_row("carregar", "Carrega a última sessão")
|
|
1369
|
+
help_table.add_row("/exit", "Sai do programa")
|
|
1370
|
+
help_table.add_row("/help", "Mostra esta ajuda")
|
|
1371
|
+
help_table.add_row("qualquer outra coisa", "Chat normal com a IA")
|
|
1372
|
+
console.print(help_table)
|
|
1373
|
+
|
|
1374
|
+
def show_header():
|
|
1375
|
+
header_lines = [
|
|
1376
|
+
"[bold cyan]🤖 claudio AUTÔNOMO[/]",
|
|
1377
|
+
f"[white]📁[/] [yellow]{WORKSPACE}[/]",
|
|
1378
|
+
f"[white]🧠[/] [green]{MODEL}[/]"
|
|
1379
|
+
]
|
|
1380
|
+
loaded = []
|
|
1381
|
+
if CLAUDIO_MD:
|
|
1382
|
+
loaded.append("claudio.md")
|
|
1383
|
+
if CLAUDIO_SKILLS:
|
|
1384
|
+
loaded.append(f"skills/{len(CLAUDIO_SKILLS)}")
|
|
1385
|
+
if CLAUDIO_RULES:
|
|
1386
|
+
n = sum(1 for _ in (WORKSPACE / ".claudio" / "RULES").iterdir()) if (WORKSPACE / ".claudio" / "RULES").exists() else 0
|
|
1387
|
+
loaded.append(f"RULES/{n}")
|
|
1388
|
+
if HISTORY_FILE.exists():
|
|
1389
|
+
loaded.append("sessão")
|
|
1390
|
+
if loaded:
|
|
1391
|
+
header_lines.append(f"[white]📄[/] [green]✓ {', '.join(loaded)}[/]")
|
|
1392
|
+
header = Panel(
|
|
1393
|
+
"\n".join(header_lines),
|
|
1394
|
+
box=box.ROUNDED,
|
|
1395
|
+
border_style="cyan",
|
|
1396
|
+
padding=(1, 2)
|
|
1397
|
+
)
|
|
1398
|
+
console.print(header)
|
|
1399
|
+
|
|
1400
|
+
def main():
|
|
1401
|
+
console.clear()
|
|
1402
|
+
show_header()
|
|
1403
|
+
|
|
1404
|
+
while True:
|
|
1405
|
+
try:
|
|
1406
|
+
user = Prompt.ask(
|
|
1407
|
+
"\n[bold cyan]💻 agent[/]"
|
|
1408
|
+
)
|
|
1409
|
+
|
|
1410
|
+
if not user:
|
|
1411
|
+
continue
|
|
1412
|
+
|
|
1413
|
+
lower = user.lower().strip()
|
|
1414
|
+
|
|
1415
|
+
if lower == "/exit":
|
|
1416
|
+
session_save()
|
|
1417
|
+
console.print("[yellow]👋 Encerrando...[/]")
|
|
1418
|
+
break
|
|
1419
|
+
|
|
1420
|
+
if lower == "/help":
|
|
1421
|
+
show_help()
|
|
1422
|
+
continue
|
|
1423
|
+
|
|
1424
|
+
if lower in ["/skills", "skills", "/skill"]:
|
|
1425
|
+
list_skills()
|
|
1426
|
+
continue
|
|
1427
|
+
|
|
1428
|
+
ANALISE_KEYWORDS = ["analisar", "analise", "analisa", "analyze", "analyse"]
|
|
1429
|
+
if any(lower.startswith(kw) for kw in ANALISE_KEYWORDS) and any(
|
|
1430
|
+
word in lower for word in ["projeto", "proyect", "project"]
|
|
1431
|
+
):
|
|
1432
|
+
analyze_project()
|
|
1433
|
+
continue
|
|
1434
|
+
|
|
1435
|
+
elif any(lower.startswith(kw) for kw in ANALISE_KEYWORDS):
|
|
1436
|
+
target = ""
|
|
1437
|
+
for word in ["pasta ", "diretorio ", "diretório ", "arquivo ", "folder ", "file "]:
|
|
1438
|
+
if word in lower:
|
|
1439
|
+
idx = lower.index(word) + len(word)
|
|
1440
|
+
target = user[idx:].strip()
|
|
1441
|
+
break
|
|
1442
|
+
if not target:
|
|
1443
|
+
parts = user.split(maxsplit=1)
|
|
1444
|
+
if len(parts) >= 2:
|
|
1445
|
+
target = parts[1].strip()
|
|
1446
|
+
for prefix in ["a ", "o ", "as ", "os ", "de ", "da ", "do ", "das ", "dos "]:
|
|
1447
|
+
if target.lower().startswith(prefix):
|
|
1448
|
+
target = target[len(prefix):].strip()
|
|
1449
|
+
if not target:
|
|
1450
|
+
render_message("error", "Informe o arquivo ou pasta")
|
|
1451
|
+
continue
|
|
1452
|
+
p_target = Path(target)
|
|
1453
|
+
full_path = p_target if p_target.is_absolute() else WORKSPACE / target
|
|
1454
|
+
render_message("info", f"[bold]> Alvo:[/] {full_path}")
|
|
1455
|
+
if full_path.exists() and full_path.is_dir():
|
|
1456
|
+
analyze_folder(str(full_path))
|
|
1457
|
+
else:
|
|
1458
|
+
analyze_code(str(full_path))
|
|
1459
|
+
continue
|
|
1460
|
+
|
|
1461
|
+
elif lower.startswith("listar") or lower in ["ls", "dir", "tree"]:
|
|
1462
|
+
parts = user.split(maxsplit=1)
|
|
1463
|
+
path = parts[1] if len(parts) > 1 else "."
|
|
1464
|
+
list_directory(path)
|
|
1465
|
+
continue
|
|
1466
|
+
|
|
1467
|
+
elif lower.startswith("ler "):
|
|
1468
|
+
parts = user.split(maxsplit=1)
|
|
1469
|
+
if len(parts) < 2:
|
|
1470
|
+
render_message("error", "Informe o arquivo")
|
|
1471
|
+
continue
|
|
1472
|
+
read_file_content(parts[1])
|
|
1473
|
+
continue
|
|
1474
|
+
|
|
1475
|
+
elif lower.startswith("editar ") or lower.startswith("modificar "):
|
|
1476
|
+
prefix_len = len("editar ") if lower.startswith("editar ") else len("modificar ")
|
|
1477
|
+
rest = user[prefix_len:].strip()
|
|
1478
|
+
first_word_end = rest.find(" ")
|
|
1479
|
+
if first_word_end == -1:
|
|
1480
|
+
render_message("error", "Use: editar <arquivo> <instrução>")
|
|
1481
|
+
continue
|
|
1482
|
+
fpath = rest[:first_word_end].strip()
|
|
1483
|
+
instr = rest[first_word_end:].strip()
|
|
1484
|
+
edit_file(fpath, instr)
|
|
1485
|
+
continue
|
|
1486
|
+
|
|
1487
|
+
elif lower.startswith("refatorar"):
|
|
1488
|
+
parts = user.split(maxsplit=1)
|
|
1489
|
+
if len(parts) < 2:
|
|
1490
|
+
render_message("error", "Use: refatorar <arquivo>")
|
|
1491
|
+
continue
|
|
1492
|
+
refactor_code(parts[1])
|
|
1493
|
+
continue
|
|
1494
|
+
|
|
1495
|
+
elif lower.startswith("debug"):
|
|
1496
|
+
parts = user.split()
|
|
1497
|
+
if len(parts) < 2:
|
|
1498
|
+
render_message("error", "Informe o arquivo")
|
|
1499
|
+
continue
|
|
1500
|
+
file_path = parts[1]
|
|
1501
|
+
error = " ".join(parts[2:])
|
|
1502
|
+
debug_code(file_path, error)
|
|
1503
|
+
continue
|
|
1504
|
+
|
|
1505
|
+
elif lower.startswith("executar"):
|
|
1506
|
+
rest = user[len("executar"):].strip()
|
|
1507
|
+
skill_target = None
|
|
1508
|
+
stem = Path(rest).stem.lower()
|
|
1509
|
+
for s in CLAUDIO_SKILLS:
|
|
1510
|
+
if s["name"].lower() == rest.lower() or Path(s["name"]).stem.lower() == stem:
|
|
1511
|
+
skill_target = s["name"]
|
|
1512
|
+
break
|
|
1513
|
+
if not skill_target and (rest.lower().startswith("skill") or rest.lower().startswith("a skill")):
|
|
1514
|
+
sr = rest[5:] if rest.lower().startswith("skill") else rest[7:]
|
|
1515
|
+
sr = sr.strip()
|
|
1516
|
+
for s in CLAUDIO_SKILLS:
|
|
1517
|
+
if s["name"].lower() == sr.lower() or Path(s["name"]).stem.lower() == Path(sr).stem.lower():
|
|
1518
|
+
skill_target = s["name"]
|
|
1519
|
+
break
|
|
1520
|
+
if skill_target:
|
|
1521
|
+
execute_skill(skill_target)
|
|
1522
|
+
else:
|
|
1523
|
+
run_terminal(rest)
|
|
1524
|
+
continue
|
|
1525
|
+
|
|
1526
|
+
elif lower.startswith("git "):
|
|
1527
|
+
git_command(user[4:].strip())
|
|
1528
|
+
continue
|
|
1529
|
+
|
|
1530
|
+
elif lower.startswith("buscar ") or lower.startswith("pesquisar "):
|
|
1531
|
+
prefix = len("buscar ") if lower.startswith("buscar ") else len("pesquisar ")
|
|
1532
|
+
web_search(user[prefix:].strip())
|
|
1533
|
+
continue
|
|
1534
|
+
|
|
1535
|
+
elif lower.startswith("site ") or lower.startswith("acessar "):
|
|
1536
|
+
prefix = len("site ") if lower.startswith("site ") else len("acessar ")
|
|
1537
|
+
fetch_url(user[prefix:].strip())
|
|
1538
|
+
continue
|
|
1539
|
+
|
|
1540
|
+
elif lower.startswith("skill "):
|
|
1541
|
+
sname = user[6:].strip()
|
|
1542
|
+
if sname:
|
|
1543
|
+
execute_skill(sname)
|
|
1544
|
+
else:
|
|
1545
|
+
list_skills()
|
|
1546
|
+
continue
|
|
1547
|
+
|
|
1548
|
+
elif lower.startswith("modelo"):
|
|
1549
|
+
parts = user.split(maxsplit=1)
|
|
1550
|
+
model_name = parts[1] if len(parts) > 1 else ""
|
|
1551
|
+
switch_model(model_name)
|
|
1552
|
+
continue
|
|
1553
|
+
|
|
1554
|
+
elif lower in ["config", "configurar", "reconfigurar", "setup"]:
|
|
1555
|
+
reconfigurar()
|
|
1556
|
+
continue
|
|
1557
|
+
|
|
1558
|
+
elif lower in ["salvar", "salvar sessão"]:
|
|
1559
|
+
session_save()
|
|
1560
|
+
render_message("info", "💾 Sessão salva")
|
|
1561
|
+
continue
|
|
1562
|
+
|
|
1563
|
+
elif lower in ["carregar", "carregar sessão", "restaurar"]:
|
|
1564
|
+
session_load()
|
|
1565
|
+
continue
|
|
1566
|
+
|
|
1567
|
+
elif lower in ["instalar", "deps", "instalar deps"]:
|
|
1568
|
+
auto_install_deps()
|
|
1569
|
+
continue
|
|
1570
|
+
|
|
1571
|
+
elif lower.startswith("autonomo") or lower.startswith("missao") or lower.startswith("missão"):
|
|
1572
|
+
prefix = next(len(p) for p in ["autonomo ", "missao ", "missão "] if lower.startswith(p))
|
|
1573
|
+
task = user[prefix:].strip()
|
|
1574
|
+
if not task:
|
|
1575
|
+
render_message("error", "Descreva a missão. Ex: autonomo crie um servidor web")
|
|
1576
|
+
continue
|
|
1577
|
+
autonomous_mode(task)
|
|
1578
|
+
continue
|
|
1579
|
+
|
|
1580
|
+
elif any(kw in lower for kw in ["crie", "criar", "gere", "faça", "desenvolva"]):
|
|
1581
|
+
create_project(user)
|
|
1582
|
+
continue
|
|
1583
|
+
|
|
1584
|
+
else:
|
|
1585
|
+
normal_chat(user)
|
|
1586
|
+
|
|
1587
|
+
except KeyboardInterrupt:
|
|
1588
|
+
console.print("\n[yellow]👋 Encerrando...[/]")
|
|
1589
|
+
break
|
|
1590
|
+
|
|
1591
|
+
except Exception as e:
|
|
1592
|
+
render_message("error", f"{e}")
|
|
1593
|
+
|
|
1594
|
+
if __name__ == "__main__":
|
|
1595
|
+
main()
|
|
1596
|
+
```
|
|
1597
|
+
|
|
1598
|
+
## Observações
|
|
1599
|
+
|
|
1600
|
+
- API Key: lida primeiro de `os.getenv("API_KEY")`, depois do `.env`
|
|
1601
|
+
- `.claudio/` contém skills e RULES carregados automaticamente no contexto
|
|
1602
|
+
- `.claudio_history.json` é gerado automaticamente ao salvar sessão
|
|
1603
|
+
- O `__pycache__/` é gerado automaticamente pelo Python
|
|
1604
|
+
- `.env` contém a chave da API — **nunca compartilhe ou comite este arquivo**
|