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.
@@ -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**