brag-cli 0.1.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.
- brag_cli/__init__.py +0 -0
- brag_cli/commands/__init__.py +0 -0
- brag_cli/commands/ai.py +39 -0
- brag_cli/commands/brags.py +0 -0
- brag_cli/commands/svc.py +55 -0
- brag_cli/config.py +33 -0
- brag_cli/llm.py +56 -0
- brag_cli/main.py +280 -0
- brag_cli/py.typed +0 -0
- brag_cli/stats.py +83 -0
- brag_cli-0.1.0.dist-info/METADATA +14 -0
- brag_cli-0.1.0.dist-info/RECORD +14 -0
- brag_cli-0.1.0.dist-info/WHEEL +4 -0
- brag_cli-0.1.0.dist-info/entry_points.txt +3 -0
brag_cli/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
brag_cli/commands/ai.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import json
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from ..config import load_secrets, save_secrets
|
|
6
|
+
|
|
7
|
+
ai_app = typer.Typer()
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@ai_app.command("config-provider")
|
|
12
|
+
def config_provider(
|
|
13
|
+
provider: str = typer.Option(..., prompt="Provider (ex: groq, openai, openrouter)"),
|
|
14
|
+
model: str = typer.Option(..., prompt="Model (ex: llama3-8b-8192, gpt-4o-mini)"),
|
|
15
|
+
):
|
|
16
|
+
"""Configura o provedor de IA e modelo no data/profile.json."""
|
|
17
|
+
profile_path = Path.cwd() / "profile.json"
|
|
18
|
+
if not profile_path.exists():
|
|
19
|
+
console.print(
|
|
20
|
+
"[red]Arquivo profile.json não encontrado na raiz. Rode `brag init` primeiro.[/red]"
|
|
21
|
+
)
|
|
22
|
+
raise typer.Exit(1)
|
|
23
|
+
|
|
24
|
+
data = json.loads(profile_path.read_text())
|
|
25
|
+
data["llm"] = {"provider": provider.lower(), "model": model}
|
|
26
|
+
profile_path.write_text(json.dumps(data, indent=2))
|
|
27
|
+
console.print(f"[green]Provedor configurado: {provider} / {model}[/green]")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@ai_app.command("config-key")
|
|
31
|
+
def config_key():
|
|
32
|
+
"""Configura a chave do provider no secrets.toml (BYOK)."""
|
|
33
|
+
api_key = typer.prompt("Insira a API Key do seu provedor", hide_input=True)
|
|
34
|
+
secrets = load_secrets()
|
|
35
|
+
secrets["api_key"] = api_key
|
|
36
|
+
save_secrets(secrets)
|
|
37
|
+
console.print(
|
|
38
|
+
"[green]API Key salva com segurança no seu secrets.toml local.[/green]"
|
|
39
|
+
)
|
|
File without changes
|
brag_cli/commands/svc.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import subprocess
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from ..config import check_local_workspace
|
|
6
|
+
|
|
7
|
+
svc_app = typer.Typer()
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
@svc_app.callback(invoke_without_command=True)
|
|
11
|
+
def svc(
|
|
12
|
+
push: bool = typer.Option(False, "--push", help="Faz commit das alterações locais e push para o repositório remoto."),
|
|
13
|
+
pull: bool = typer.Option(False, "--pull", help="Faz pull das alterações mais recentes do repositório remoto.")
|
|
14
|
+
):
|
|
15
|
+
"""Sincroniza o repositório local (abstrai git pull e git push)."""
|
|
16
|
+
if not push and not pull:
|
|
17
|
+
console.print("[yellow]Forneça --push ou --pull[/yellow]")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
is_valid, msg = check_local_workspace()
|
|
21
|
+
if not is_valid:
|
|
22
|
+
console.print(f"[red]Erro:[/red] {msg}")
|
|
23
|
+
console.print("Rode [cyan]brag init[/cyan] ou entre no diretório correto.")
|
|
24
|
+
raise typer.Exit(1)
|
|
25
|
+
|
|
26
|
+
if pull:
|
|
27
|
+
console.print("[dim]Executando git pull...[/dim]")
|
|
28
|
+
try:
|
|
29
|
+
subprocess.run(["git", "pull"], check=True)
|
|
30
|
+
console.print("[green]Sincronizado com sucesso (pull).[/green]")
|
|
31
|
+
except subprocess.CalledProcessError:
|
|
32
|
+
console.print("[red]Falha ao executar git pull.[/red]")
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
|
|
35
|
+
if push:
|
|
36
|
+
console.print("[dim]Adicionando alterações ao git...[/dim]")
|
|
37
|
+
now = datetime.now()
|
|
38
|
+
commit_msg = f"{now.strftime('%Y-%m-%d %H:%M:%S')} - add entry for {now.strftime('%m')}/{now.strftime('%Y')}"
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
subprocess.run(["git", "add", "data/"], check=True)
|
|
42
|
+
status = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True)
|
|
43
|
+
if not status.stdout.strip():
|
|
44
|
+
console.print("[yellow]Nenhuma alteração em 'data/' para enviar.[/yellow]")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
subprocess.run(["git", "commit", "-m", commit_msg], check=True)
|
|
48
|
+
console.print(f"[dim]Commit gerado: '{commit_msg}'[/dim]")
|
|
49
|
+
|
|
50
|
+
console.print("[dim]Executando git push...[/dim]")
|
|
51
|
+
subprocess.run(["git", "push"], check=True)
|
|
52
|
+
console.print("[green]Alterações enviadas com sucesso (push).[/green]")
|
|
53
|
+
except subprocess.CalledProcessError as e:
|
|
54
|
+
console.print(f"[red]Falha ao sincronizar (push). Verifique seu remoto.[/red]")
|
|
55
|
+
raise typer.Exit(1)
|
brag_cli/config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import tomlkit
|
|
4
|
+
|
|
5
|
+
def get_secrets_file() -> Path:
|
|
6
|
+
return Path.cwd() / "secrets.toml"
|
|
7
|
+
|
|
8
|
+
def load_secrets() -> dict:
|
|
9
|
+
secrets_file = get_secrets_file()
|
|
10
|
+
if not secrets_file.exists():
|
|
11
|
+
return {}
|
|
12
|
+
try:
|
|
13
|
+
with secrets_file.open("r") as f:
|
|
14
|
+
return tomlkit.load(f)
|
|
15
|
+
except Exception:
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
def save_secrets(secrets: dict):
|
|
19
|
+
secrets_file = get_secrets_file()
|
|
20
|
+
with secrets_file.open("w") as f:
|
|
21
|
+
tomlkit.dump(secrets, f)
|
|
22
|
+
|
|
23
|
+
def check_local_workspace():
|
|
24
|
+
"""Valida se o diretório atual é um workspace válido do braguissimo."""
|
|
25
|
+
cwd = Path.cwd()
|
|
26
|
+
|
|
27
|
+
if not (cwd / ".git").exists():
|
|
28
|
+
return False, "O diretório atual não é um repositório git."
|
|
29
|
+
|
|
30
|
+
if not (cwd / "profile.json").exists():
|
|
31
|
+
return False, "Arquivo 'profile.json' não encontrado no diretório atual."
|
|
32
|
+
|
|
33
|
+
return True, "OK"
|
brag_cli/llm.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
console = Console()
|
|
5
|
+
|
|
6
|
+
class LLMClient:
|
|
7
|
+
def __init__(self, provider: str, model: str, api_key: str):
|
|
8
|
+
self.provider = provider.lower()
|
|
9
|
+
self.model = model
|
|
10
|
+
self.api_key = api_key
|
|
11
|
+
|
|
12
|
+
if self.provider == "groq":
|
|
13
|
+
self.base_url = "https://api.groq.com/openai/v1"
|
|
14
|
+
elif self.provider == "openai":
|
|
15
|
+
self.base_url = "https://api.openai.com/v1"
|
|
16
|
+
elif self.provider == "openrouter":
|
|
17
|
+
self.base_url = "https://openrouter.ai/api/v1"
|
|
18
|
+
else:
|
|
19
|
+
# Fallback to standard OpenAI compatibility format
|
|
20
|
+
self.base_url = "https://api.openai.com/v1"
|
|
21
|
+
|
|
22
|
+
def restructure_brag(self, title: str, description: str) -> str:
|
|
23
|
+
prompt = (
|
|
24
|
+
"Atue como um mentor de carreira especialista. Você receberá o título e a descrição de uma "
|
|
25
|
+
"conquista profissional (um 'brag'). Reescreva a descrição para torná-la mais profissional, "
|
|
26
|
+
"clara, focada em impacto e métricas, mas mantenha o texto em primeira pessoa. Não invente dados. "
|
|
27
|
+
"Responda APENAS com o texto reestruturado, sem introduções ou explicações.\n\n"
|
|
28
|
+
f"Título: {title}\n"
|
|
29
|
+
f"Descrição original: {description}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
payload = {
|
|
33
|
+
"model": self.model,
|
|
34
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
35
|
+
"temperature": 0.3
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
headers = {
|
|
39
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
40
|
+
"Content-Type": "application/json"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
with console.status(f"[dim]Reestruturando brag usando {self.provider} ({self.model})...[/dim]"):
|
|
44
|
+
try:
|
|
45
|
+
resp = httpx.post(f"{self.base_url}/chat/completions", json=payload, headers=headers, timeout=30.0)
|
|
46
|
+
resp.raise_for_status()
|
|
47
|
+
data = resp.json()
|
|
48
|
+
return data["choices"][0]["message"]["content"].strip()
|
|
49
|
+
except httpx.HTTPError as e:
|
|
50
|
+
console.print(f"[red]Erro na chamada LLM: {e}[/red]")
|
|
51
|
+
if hasattr(e, 'response') and e.response:
|
|
52
|
+
console.print(f"[dim]{e.response.text}[/dim]")
|
|
53
|
+
return description
|
|
54
|
+
except Exception as e:
|
|
55
|
+
console.print(f"[red]Erro interno no LLM: {e}[/red]")
|
|
56
|
+
return description
|
brag_cli/main.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import json
|
|
5
|
+
import uuid
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .commands.svc import svc_app
|
|
10
|
+
from .commands.ai import ai_app
|
|
11
|
+
from .config import check_local_workspace
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(name="braguissimo", help="CLI para registrar suas conquistas (Local + BYOS).")
|
|
14
|
+
app.add_typer(svc_app, name="svc", help="Sincronização do repositório (push/pull)")
|
|
15
|
+
app.add_typer(ai_app, name="ai", help="Comandos de IA e configuração de BYOK")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
@app.command()
|
|
19
|
+
def init():
|
|
20
|
+
"""Inicializa um repositório local do braguissimo no diretório atual."""
|
|
21
|
+
cwd = Path.cwd()
|
|
22
|
+
|
|
23
|
+
if (cwd / ".git").exists() and (cwd / "profile.json").exists():
|
|
24
|
+
console.print("[yellow]O repositório já está inicializado neste diretório.[/yellow]")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
console.print("[dim]Inicializando repositório Git local...[/dim]")
|
|
28
|
+
try:
|
|
29
|
+
subprocess.run(["git", "init"], check=True, capture_output=True)
|
|
30
|
+
except subprocess.CalledProcessError:
|
|
31
|
+
console.print("[red]Falha ao rodar git init. Você tem o git instalado?[/red]")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
|
|
34
|
+
owner = typer.prompt("Nome de usuário (github username)", default="")
|
|
35
|
+
|
|
36
|
+
# Criar profile.json na raiz
|
|
37
|
+
profile_path = cwd / "profile.json"
|
|
38
|
+
if not profile_path.exists():
|
|
39
|
+
profile_path.write_text(json.dumps({
|
|
40
|
+
"name": owner,
|
|
41
|
+
"created_at": datetime.utcnow().isoformat() + "Z",
|
|
42
|
+
"initialized": True
|
|
43
|
+
}, indent=2))
|
|
44
|
+
|
|
45
|
+
# Criar .gitignore
|
|
46
|
+
gitignore_path = cwd / ".gitignore"
|
|
47
|
+
ignores = ["secrets.toml", ".venv/", "__pycache__/"]
|
|
48
|
+
if not gitignore_path.exists():
|
|
49
|
+
gitignore_path.write_text("\n".join(ignores) + "\n")
|
|
50
|
+
else:
|
|
51
|
+
content = gitignore_path.read_text()
|
|
52
|
+
to_append = [ign for ign in ignores if ign not in content]
|
|
53
|
+
if to_append:
|
|
54
|
+
with gitignore_path.open("a") as f:
|
|
55
|
+
f.write("\n" + "\n".join(to_append) + "\n")
|
|
56
|
+
|
|
57
|
+
console.print("[green]Repositório inicializado com sucesso![/green]")
|
|
58
|
+
console.print("Adicione seu remote com: [cyan]git remote add origin <sua-url>[/cyan]")
|
|
59
|
+
|
|
60
|
+
@app.command()
|
|
61
|
+
def add(title: str = typer.Option(None, "--title", "-t", help="Título da conquista"),
|
|
62
|
+
description: str = typer.Option(None, "--message", "-m", help="Descrição livre"),
|
|
63
|
+
tags: str = typer.Option("", "--tags", help="Tags separadas por vírgula")):
|
|
64
|
+
"""Adiciona um novo brag localmente."""
|
|
65
|
+
is_valid, msg = check_local_workspace()
|
|
66
|
+
if not is_valid:
|
|
67
|
+
console.print(f"[red]{msg}[/red]")
|
|
68
|
+
console.print("Rode [cyan]brag init[/cyan] primeiro neste diretório.")
|
|
69
|
+
raise typer.Exit(1)
|
|
70
|
+
|
|
71
|
+
date_input = typer.prompt("Data (DD/MM/AAAA) [Deixe vazio para hoje]", default="", show_default=False)
|
|
72
|
+
if not date_input:
|
|
73
|
+
target_date = datetime.utcnow()
|
|
74
|
+
else:
|
|
75
|
+
try:
|
|
76
|
+
target_date = datetime.strptime(date_input, "%d/%m/%Y")
|
|
77
|
+
except ValueError:
|
|
78
|
+
console.print("[red]Formato de data inválido. Use DD/MM/AAAA.[/red]")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
if not title:
|
|
82
|
+
title = typer.prompt("Título da conquista")
|
|
83
|
+
if not description:
|
|
84
|
+
description = typer.prompt("Descrição")
|
|
85
|
+
if not tags:
|
|
86
|
+
tags = typer.prompt("Tags (separadas por vírgula)", default="", show_default=False)
|
|
87
|
+
|
|
88
|
+
# AI Reestruturação
|
|
89
|
+
from .llm import LLMClient
|
|
90
|
+
from .config import load_secrets
|
|
91
|
+
|
|
92
|
+
profile_path = Path.cwd() / "profile.json"
|
|
93
|
+
secrets = load_secrets()
|
|
94
|
+
api_key = secrets.get("api_key")
|
|
95
|
+
|
|
96
|
+
if api_key and profile_path.exists():
|
|
97
|
+
try:
|
|
98
|
+
profile_data = json.loads(profile_path.read_text())
|
|
99
|
+
llm_config = profile_data.get("llm")
|
|
100
|
+
if llm_config and llm_config.get("provider") and llm_config.get("model"):
|
|
101
|
+
want_ai = typer.confirm("Você quer reestruturar o brag com IA?", default=True)
|
|
102
|
+
if want_ai:
|
|
103
|
+
client = LLMClient(llm_config["provider"], llm_config["model"], api_key)
|
|
104
|
+
new_desc = client.restructure_brag(title, description)
|
|
105
|
+
console.print("\n[dim]--- Texto sugerido ---[/dim]")
|
|
106
|
+
console.print(new_desc)
|
|
107
|
+
console.print("[dim]------------------------[/dim]\n")
|
|
108
|
+
if typer.confirm("Deseja usar essa nova descrição?", default=True):
|
|
109
|
+
description = new_desc
|
|
110
|
+
except Exception as e:
|
|
111
|
+
console.print(f"[dim]Falha ao carregar IA: {e}[/dim]")
|
|
112
|
+
|
|
113
|
+
tag_list = [t.strip() for t in tags.split(",") if t.strip()]
|
|
114
|
+
|
|
115
|
+
brag = {
|
|
116
|
+
"id": str(uuid.uuid4()),
|
|
117
|
+
"date": target_date.isoformat() + "Z",
|
|
118
|
+
"title": title,
|
|
119
|
+
"description": description,
|
|
120
|
+
"tags": tag_list
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
month_dir = Path.cwd() / "data" / "entries" / target_date.strftime('%Y')
|
|
124
|
+
month_dir.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
path = month_dir / f"{target_date.strftime('%m')}.json"
|
|
127
|
+
|
|
128
|
+
if path.exists():
|
|
129
|
+
data = json.loads(path.read_text())
|
|
130
|
+
data.setdefault("entries", []).append(brag)
|
|
131
|
+
else:
|
|
132
|
+
data = {
|
|
133
|
+
"month": target_date.strftime('%B'),
|
|
134
|
+
"year": target_date.year,
|
|
135
|
+
"entries": [brag]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
path.write_text(json.dumps(data, indent=2))
|
|
139
|
+
console.print(f"[green]Brag adicionado localmente! (ID: {brag['id']})[/green]")
|
|
140
|
+
console.print("Use [cyan]brag svc --push[/cyan] para enviar ao repositório remoto.")
|
|
141
|
+
|
|
142
|
+
@app.command()
|
|
143
|
+
def list(month: str = typer.Option(None, help="Mês no formato MM"),
|
|
144
|
+
year: str = typer.Option(None, help="Ano no formato YYYY")):
|
|
145
|
+
"""Lista os brags locais do período."""
|
|
146
|
+
is_valid, msg = check_local_workspace()
|
|
147
|
+
if not is_valid:
|
|
148
|
+
console.print(f"[red]{msg}[/red]")
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
|
|
151
|
+
now = datetime.now()
|
|
152
|
+
m = month or now.strftime('%m')
|
|
153
|
+
y = year or now.strftime('%Y')
|
|
154
|
+
|
|
155
|
+
path = Path.cwd() / "data" / "entries" / y / f"{m}.json"
|
|
156
|
+
|
|
157
|
+
if not path.exists():
|
|
158
|
+
console.print("[yellow]Nenhum brag encontrado localmente para este período.[/yellow]")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
data = json.loads(path.read_text())
|
|
162
|
+
entries = data.get("entries", [])
|
|
163
|
+
|
|
164
|
+
if not entries:
|
|
165
|
+
console.print("[yellow]Nenhum brag encontrado localmente para este período.[/yellow]")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
for entry in entries:
|
|
169
|
+
console.print(f"\n[cyan]{entry['title']}[/cyan] ({entry.get('date', '')})")
|
|
170
|
+
console.print(f"ID: [dim]{entry['id']}[/dim]")
|
|
171
|
+
console.print(f"{entry['description']}")
|
|
172
|
+
if entry.get('tags'):
|
|
173
|
+
console.print(f"Tags: {', '.join(entry['tags'])}")
|
|
174
|
+
|
|
175
|
+
@app.command()
|
|
176
|
+
def edit(id: str):
|
|
177
|
+
"""Permite editar um brag local específico."""
|
|
178
|
+
console.print(f"[yellow]O arquivo é local! Você pode editá-lo diretamente em `data/entries/` com seu editor preferido.[/yellow]")
|
|
179
|
+
|
|
180
|
+
@app.command()
|
|
181
|
+
def delete(id: str):
|
|
182
|
+
"""Remove um brag específico."""
|
|
183
|
+
console.print(f"[yellow]O arquivo é local! Você pode remover a entrada do JSON em `data/entries/`.[/yellow]")
|
|
184
|
+
|
|
185
|
+
@app.command()
|
|
186
|
+
def status():
|
|
187
|
+
"""Exibe o status do repositório local e métricas rápidas."""
|
|
188
|
+
is_valid, msg = check_local_workspace()
|
|
189
|
+
if not is_valid:
|
|
190
|
+
console.print(f"[red]{msg}[/red]")
|
|
191
|
+
raise typer.Exit(1)
|
|
192
|
+
|
|
193
|
+
from .stats import get_git_info, get_all_brags, get_current_month_count
|
|
194
|
+
from rich.table import Table
|
|
195
|
+
from rich.panel import Panel
|
|
196
|
+
|
|
197
|
+
profile_path = Path.cwd() / "profile.json"
|
|
198
|
+
owner = "Desconhecido"
|
|
199
|
+
if profile_path.exists():
|
|
200
|
+
try:
|
|
201
|
+
owner = json.loads(profile_path.read_text()).get("name", "Desconhecido")
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
git_info = get_git_info()
|
|
206
|
+
brags = get_all_brags()
|
|
207
|
+
current_count = get_current_month_count(brags)
|
|
208
|
+
|
|
209
|
+
table = Table(show_header=False, box=None, expand=True)
|
|
210
|
+
table.add_column("Chave", style="dim", width=25)
|
|
211
|
+
table.add_column("Valor", style="bold")
|
|
212
|
+
|
|
213
|
+
table.add_row("Usuário", f"[cyan]{owner}[/cyan]")
|
|
214
|
+
table.add_row("Repositório Remoto", f"{git_info['remote_url']}")
|
|
215
|
+
|
|
216
|
+
changes_str = "[yellow]Existem alterações não sincronizadas[/yellow]" if git_info['has_changes'] else "[green]Tudo sincronizado[/green]"
|
|
217
|
+
table.add_row("Status Git", changes_str)
|
|
218
|
+
|
|
219
|
+
table.add_row("Brags neste mês", f"{current_count}")
|
|
220
|
+
table.add_row("Brags no total", f"{len(brags)}")
|
|
221
|
+
|
|
222
|
+
panel = Panel(table, title="📊 Braguíssimo Status", border_style="blue", expand=True)
|
|
223
|
+
console.print(panel)
|
|
224
|
+
|
|
225
|
+
@app.command()
|
|
226
|
+
def analytics():
|
|
227
|
+
"""Exibe um dashboard com métricas detalhadas de uso."""
|
|
228
|
+
is_valid, msg = check_local_workspace()
|
|
229
|
+
if not is_valid:
|
|
230
|
+
console.print(f"[red]{msg}[/red]")
|
|
231
|
+
raise typer.Exit(1)
|
|
232
|
+
|
|
233
|
+
from .stats import get_all_brags, get_monthly_stats, get_tag_stats
|
|
234
|
+
from rich.table import Table
|
|
235
|
+
from rich.panel import Panel
|
|
236
|
+
|
|
237
|
+
brags = get_all_brags()
|
|
238
|
+
if not brags:
|
|
239
|
+
console.print("[yellow]Você ainda não registrou nenhum brag para analisar.[/yellow]")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
counts, top_month, top_count = get_monthly_stats(brags)
|
|
243
|
+
tags_freq = get_tag_stats(brags)
|
|
244
|
+
|
|
245
|
+
# 1. Tabela Geral
|
|
246
|
+
overview_table = Table(title="Visão Geral", show_header=False, box=None, expand=True)
|
|
247
|
+
overview_table.add_column("Métrica", style="dim")
|
|
248
|
+
overview_table.add_column("Valor", style="bold")
|
|
249
|
+
overview_table.add_row("Total de Entradas", str(len(brags)))
|
|
250
|
+
if top_month:
|
|
251
|
+
overview_table.add_row("Mês Mais Produtivo", f"{top_month} ({top_count} brags)")
|
|
252
|
+
|
|
253
|
+
# 2. Distribuição de Tags
|
|
254
|
+
tag_table = Table(title="Top Tags", show_edge=False, header_style="dim", expand=True)
|
|
255
|
+
tag_table.add_column("Tag")
|
|
256
|
+
tag_table.add_column("Uso", justify="right")
|
|
257
|
+
|
|
258
|
+
for tag, freq in tags_freq[:5]:
|
|
259
|
+
tag_table.add_row(f"[cyan]{tag}[/cyan]", str(freq))
|
|
260
|
+
|
|
261
|
+
if not tags_freq:
|
|
262
|
+
tag_table.add_row("Nenhuma tag", "0")
|
|
263
|
+
|
|
264
|
+
# 3. Timeline de Meses
|
|
265
|
+
timeline_table = Table(title="Atividade por Mês", show_edge=False, header_style="dim", expand=True)
|
|
266
|
+
timeline_table.add_column("Mês")
|
|
267
|
+
timeline_table.add_column("Volume", justify="left")
|
|
268
|
+
timeline_table.add_column("Qtd", justify="right")
|
|
269
|
+
|
|
270
|
+
for month_key in sorted(counts.keys()):
|
|
271
|
+
qtd = counts[month_key]
|
|
272
|
+
bar = "█" * min(qtd, 40)
|
|
273
|
+
timeline_table.add_row(month_key, f"[green]{bar}[/green]", str(qtd))
|
|
274
|
+
|
|
275
|
+
console.print(Panel(overview_table, border_style="blue", expand=True))
|
|
276
|
+
console.print(Panel(tag_table, border_style="cyan", expand=True))
|
|
277
|
+
console.print(Panel(timeline_table, border_style="green", expand=True))
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
app()
|
brag_cli/py.typed
ADDED
|
File without changes
|
brag_cli/stats.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from collections import Counter, defaultdict
|
|
6
|
+
|
|
7
|
+
def get_git_info():
|
|
8
|
+
"""Retorna um dicionário com status git (has_changes, remote_url)."""
|
|
9
|
+
info = {"has_changes": False, "remote_url": "Sem remote configurado"}
|
|
10
|
+
try:
|
|
11
|
+
# Pega a origin URL
|
|
12
|
+
remote = subprocess.run(["git", "remote", "get-url", "origin"], capture_output=True, text=True)
|
|
13
|
+
if remote.returncode == 0:
|
|
14
|
+
info["remote_url"] = remote.stdout.strip()
|
|
15
|
+
|
|
16
|
+
# Pega as modificações locais em data/ ou em tudo
|
|
17
|
+
status = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True)
|
|
18
|
+
if status.stdout.strip():
|
|
19
|
+
info["has_changes"] = True
|
|
20
|
+
except FileNotFoundError:
|
|
21
|
+
pass
|
|
22
|
+
except subprocess.CalledProcessError:
|
|
23
|
+
pass
|
|
24
|
+
return info
|
|
25
|
+
|
|
26
|
+
def get_all_brags():
|
|
27
|
+
"""Lê todos os JSONs e retorna a lista combinada de todos os brags."""
|
|
28
|
+
cwd = Path.cwd()
|
|
29
|
+
entries_dir = cwd / "data" / "entries"
|
|
30
|
+
|
|
31
|
+
all_brags = []
|
|
32
|
+
if not entries_dir.exists():
|
|
33
|
+
return all_brags
|
|
34
|
+
|
|
35
|
+
for json_file in entries_dir.rglob("*.json"):
|
|
36
|
+
try:
|
|
37
|
+
data = json.loads(json_file.read_text())
|
|
38
|
+
for entry in data.get("entries", []):
|
|
39
|
+
all_brags.append(entry)
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# Sort por date descendente
|
|
44
|
+
all_brags.sort(key=lambda x: x.get("date", ""), reverse=True)
|
|
45
|
+
return all_brags
|
|
46
|
+
|
|
47
|
+
def get_monthly_stats(brags):
|
|
48
|
+
"""
|
|
49
|
+
Agrupa os brags por YYYY-MM.
|
|
50
|
+
Retorna (timeline_dict, top_month, top_month_count).
|
|
51
|
+
"""
|
|
52
|
+
counts = defaultdict(int)
|
|
53
|
+
for b in brags:
|
|
54
|
+
date_str = b.get("date", "")
|
|
55
|
+
if len(date_str) >= 7:
|
|
56
|
+
month_key = date_str[:7]
|
|
57
|
+
counts[month_key] += 1
|
|
58
|
+
|
|
59
|
+
top_month = None
|
|
60
|
+
top_count = 0
|
|
61
|
+
if counts:
|
|
62
|
+
top_month = max(counts, key=counts.get)
|
|
63
|
+
top_count = counts[top_month]
|
|
64
|
+
|
|
65
|
+
return counts, top_month, top_count
|
|
66
|
+
|
|
67
|
+
def get_tag_stats(brags):
|
|
68
|
+
"""Calcula frequência de tags."""
|
|
69
|
+
tag_counter = Counter()
|
|
70
|
+
for b in brags:
|
|
71
|
+
for t in b.get("tags", []):
|
|
72
|
+
if t:
|
|
73
|
+
tag_counter[t] += 1
|
|
74
|
+
return tag_counter.most_common()
|
|
75
|
+
|
|
76
|
+
def get_current_month_count(brags):
|
|
77
|
+
now_prefix = datetime.now().strftime("%Y-%m")
|
|
78
|
+
count = 0
|
|
79
|
+
for b in brags:
|
|
80
|
+
date_str = b.get("date", "")
|
|
81
|
+
if date_str.startswith(now_prefix):
|
|
82
|
+
count += 1
|
|
83
|
+
return count
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: brag-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Antonio Henrique Machado
|
|
6
|
+
Author-email: Antonio Henrique Machado <machadoah@proton.me>
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: pydantic>=2.12.5
|
|
9
|
+
Requires-Dist: rich>=15.0.0
|
|
10
|
+
Requires-Dist: tomlkit>=0.15.0
|
|
11
|
+
Requires-Dist: typer>=0.23.1
|
|
12
|
+
Requires-Python: >=3.14
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
brag_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
brag_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
brag_cli/commands/ai.py,sha256=KUhC4y8BiclbLAi5l8OlDBBmbKN-VUpCPGUzXqP8Ts0,1357
|
|
4
|
+
brag_cli/commands/brags.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
brag_cli/commands/svc.py,sha256=x56os1JmXeYz6hmvdui52tb1ugpJJu6GVVycmkQ7T1I,2430
|
|
6
|
+
brag_cli/config.py,sha256=3XVpsszFzYcrbZoIyWtSfgJviIKMVa1KDuRoaE6gLYU,923
|
|
7
|
+
brag_cli/llm.py,sha256=69xVIpeMKwhugTXkaevQdnAwbxSfU0DwvGhhXhHNhc8,2413
|
|
8
|
+
brag_cli/main.py,sha256=jSu7zm0jLBQ9GpjcutibTmymhtCbKMAFHmkqs__fNUw,10868
|
|
9
|
+
brag_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
brag_cli/stats.py,sha256=-EYAqQZoJofgBTO1ILMKmLW9PD84P60SejKpbWE9BcQ,2536
|
|
11
|
+
brag_cli-0.1.0.dist-info/WHEEL,sha256=s_zqWxHFEH8b58BCtf46hFCqPaISurdB9R1XJ8za6XI,80
|
|
12
|
+
brag_cli-0.1.0.dist-info/entry_points.txt,sha256=d1YnJM3l0X1DUQXesR3bahVUGwFWV7shJGTcEd8DzU4,44
|
|
13
|
+
brag_cli-0.1.0.dist-info/METADATA,sha256=nBsuIUhhAZ7b9T66YH4klDVEpO9_hM3hKC9_eGryTok,395
|
|
14
|
+
brag_cli-0.1.0.dist-info/RECORD,,
|