claudio-agent 2.0.0__py3-none-any.whl

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