wuptracker 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.
- wuptracker/__init__.py +3 -0
- wuptracker/capture.py +342 -0
- wuptracker/cli.py +187 -0
- wuptracker/common.py +382 -0
- wuptracker/config.py +156 -0
- wuptracker/configtool.py +561 -0
- wuptracker/generate.py +282 -0
- wuptracker/llm.py +315 -0
- wuptracker/paths.py +25 -0
- wuptracker/profiles.py +293 -0
- wuptracker-0.1.0.dist-info/METADATA +442 -0
- wuptracker-0.1.0.dist-info/RECORD +15 -0
- wuptracker-0.1.0.dist-info/WHEEL +4 -0
- wuptracker-0.1.0.dist-info/entry_points.txt +2 -0
- wuptracker-0.1.0.dist-info/licenses/LICENSE +21 -0
wuptracker/configtool.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""`wuptracker config` — inspeciona e altera configurações e integrações."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from . import common
|
|
7
|
+
from . import config
|
|
8
|
+
from . import paths
|
|
9
|
+
from . import profiles
|
|
10
|
+
|
|
11
|
+
ENV_PATH = paths.ENV_FILE
|
|
12
|
+
|
|
13
|
+
# chave -> (tipo, opções|None, ajuda)
|
|
14
|
+
SETTABLE = {
|
|
15
|
+
"BACKEND": ("choice",
|
|
16
|
+
["claude_cli", "anthropic", "ollama", "openai", "gemini"],
|
|
17
|
+
"motor do LLM"),
|
|
18
|
+
"CLAUDE_MODEL": ("str", None, "modelo do backend 'anthropic'"),
|
|
19
|
+
"CLAUDE_CLI_MODEL": ("str_or_none", None,
|
|
20
|
+
"modelo p/ 'claude_cli' (vazio = padrão do CLI)"),
|
|
21
|
+
"OLLAMA_HOST": ("str", None, "URL do Ollama (backend 'ollama')"),
|
|
22
|
+
"OLLAMA_MODEL": ("str", None, "modelo com visão no Ollama (llava, llama3.2-vision…)"),
|
|
23
|
+
"OPENAI_BASE_URL": ("str", None, "endpoint compatível OpenAI (backend 'openai')"),
|
|
24
|
+
"OPENAI_MODEL": ("str", None, "modelo do backend 'openai'"),
|
|
25
|
+
"GEMINI_MODEL": ("str", None, "modelo do backend 'gemini' (gemini-2.0-flash…)"),
|
|
26
|
+
"DEFAULT_PROFILE": ("domain", None, "perfil padrão: thm | generico"),
|
|
27
|
+
"DEFAULT_STYLE": ("style", None,
|
|
28
|
+
"estilo(s) padrão do writeup, vírgula p/ vários"),
|
|
29
|
+
"CAPTURE_ACTIVE_MONITOR_ONLY": ("bool", None,
|
|
30
|
+
"capturar só o monitor sob o cursor"),
|
|
31
|
+
"CROP_IMAGES": ("bool", None, "recortar imagens para a região relevante"),
|
|
32
|
+
"CROP_PADDING": ("float", None, "margem do recorte (fração, ex: 0.03)"),
|
|
33
|
+
"EMBED_IMAGES": ("bool", None, "embutir imagens no writeup"),
|
|
34
|
+
"WEBP_IMAGES": ("bool", None, "converter imagens do writeup para WebP"),
|
|
35
|
+
"WEBP_QUALITY": ("int", None, "qualidade do WebP (0-100)"),
|
|
36
|
+
"IMAGE_MAX_WIDTH": ("int", None, "largura máx. das imagens do writeup (0 = livre)"),
|
|
37
|
+
"SCREENSHOT_QUALITY": ("int", None, "qualidade JPEG no envio à API (1-100)"),
|
|
38
|
+
"PLAY_SOUND": ("bool", None, "bip ao capturar"),
|
|
39
|
+
"SHOW_NOTIFICATION": ("bool", None, "notificação de desktop ao capturar"),
|
|
40
|
+
"OPEN_AFTER_GENERATE": ("bool", None, "abrir o writeup no editor após gerar"),
|
|
41
|
+
"SESSIONS_DIR": ("str", None, "pasta das sessões"),
|
|
42
|
+
"WRITEUPS_DIR": ("str", None, "pasta dos writeups"),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ALIASES = {
|
|
46
|
+
"backend": "BACKEND", "model": "CLAUDE_MODEL", "cli-model": "CLAUDE_CLI_MODEL",
|
|
47
|
+
"ollama-host": "OLLAMA_HOST", "ollama-model": "OLLAMA_MODEL",
|
|
48
|
+
"openai-url": "OPENAI_BASE_URL", "openai-model": "OPENAI_MODEL",
|
|
49
|
+
"gemini-model": "GEMINI_MODEL",
|
|
50
|
+
"profile": "DEFAULT_PROFILE", "style": "DEFAULT_STYLE",
|
|
51
|
+
"monitor": "CAPTURE_ACTIVE_MONITOR_ONLY", "crop": "CROP_IMAGES",
|
|
52
|
+
"images": "EMBED_IMAGES", "sound": "PLAY_SOUND", "notify": "SHOW_NOTIFICATION",
|
|
53
|
+
"open": "OPEN_AFTER_GENERATE", "webp": "WEBP_IMAGES",
|
|
54
|
+
"webp-quality": "WEBP_QUALITY", "max-width": "IMAGE_MAX_WIDTH",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_key(key):
|
|
59
|
+
k = ALIASES.get(key.lower(), key.upper())
|
|
60
|
+
if k not in SETTABLE:
|
|
61
|
+
raise SystemExit(
|
|
62
|
+
f"[!] Chave desconhecida: {key!r}. Veja 'wuptracker config'.\n"
|
|
63
|
+
f" Disponíveis: {', '.join(sorted(SETTABLE))}")
|
|
64
|
+
return k
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _coerce(key, raw):
|
|
68
|
+
kind, opts, _ = SETTABLE[key]
|
|
69
|
+
if kind == "bool":
|
|
70
|
+
if raw.lower() in ("1", "true", "on", "yes", "sim", "y"):
|
|
71
|
+
return True
|
|
72
|
+
if raw.lower() in ("0", "false", "off", "no", "nao", "não", "n"):
|
|
73
|
+
return False
|
|
74
|
+
raise SystemExit(f"[!] {key} espera on/off (recebido {raw!r})")
|
|
75
|
+
if kind == "int":
|
|
76
|
+
return int(raw)
|
|
77
|
+
if kind == "float":
|
|
78
|
+
return float(raw)
|
|
79
|
+
if kind == "str_or_none":
|
|
80
|
+
return None if raw.strip().lower() in ("", "none", "-") else raw
|
|
81
|
+
if kind == "choice":
|
|
82
|
+
if raw not in opts:
|
|
83
|
+
raise SystemExit(f"[!] {key} deve ser um de: {', '.join(opts)}")
|
|
84
|
+
return raw
|
|
85
|
+
if kind == "domain":
|
|
86
|
+
return profiles.resolve_domain(raw)
|
|
87
|
+
if kind == "style":
|
|
88
|
+
return ",".join(profiles.resolve_style(s) for s in raw.split(","))
|
|
89
|
+
return raw
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _fmt(v):
|
|
93
|
+
if isinstance(v, bool):
|
|
94
|
+
return "on" if v else "off"
|
|
95
|
+
if v is None:
|
|
96
|
+
return "—"
|
|
97
|
+
return str(v)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --------------------------------------------------------------------------
|
|
101
|
+
# .env / API keys
|
|
102
|
+
# --------------------------------------------------------------------------
|
|
103
|
+
BACKEND_ENV_KEY = {"anthropic": "ANTHROPIC_API_KEY", "openai": "OPENAI_API_KEY",
|
|
104
|
+
"gemini": "GEMINI_API_KEY"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def read_env_key(name):
|
|
108
|
+
try:
|
|
109
|
+
with open(ENV_PATH, encoding="utf-8") as f:
|
|
110
|
+
for line in f:
|
|
111
|
+
if line.strip().startswith(f"{name}="):
|
|
112
|
+
return line.split("=", 1)[1].strip()
|
|
113
|
+
except OSError:
|
|
114
|
+
pass
|
|
115
|
+
return os.environ.get(name, "")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def set_env_key(name, value):
|
|
119
|
+
lines, found = [], False
|
|
120
|
+
try:
|
|
121
|
+
with open(ENV_PATH, encoding="utf-8") as f:
|
|
122
|
+
lines = f.read().splitlines()
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
for i, line in enumerate(lines):
|
|
126
|
+
if line.strip().startswith(f"{name}="):
|
|
127
|
+
lines[i] = f"{name}={value}"
|
|
128
|
+
found = True
|
|
129
|
+
break
|
|
130
|
+
if not found:
|
|
131
|
+
lines.append(f"{name}={value}")
|
|
132
|
+
paths.ensure_dirs()
|
|
133
|
+
with open(ENV_PATH, "w", encoding="utf-8") as f:
|
|
134
|
+
f.write("\n".join(lines) + "\n")
|
|
135
|
+
os.chmod(ENV_PATH, 0o600)
|
|
136
|
+
print(common.c_green(f"✓ {name} gravada em {ENV_PATH} (chmod 600)"))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def set_api_key(value, name=None):
|
|
140
|
+
"""`config api-key <valor>` — grava a chave do backend ativo (ou de `name`)."""
|
|
141
|
+
if name is None:
|
|
142
|
+
from . import llm
|
|
143
|
+
|
|
144
|
+
b = llm.resolve_backend(config.BACKEND)
|
|
145
|
+
name = BACKEND_ENV_KEY.get(b)
|
|
146
|
+
if not name:
|
|
147
|
+
raise SystemExit(
|
|
148
|
+
f"[!] o backend '{b}' não usa API key. "
|
|
149
|
+
"Troque para 'anthropic' ou 'openai' primeiro, ou use "
|
|
150
|
+
"'wuptracker config set <NOME_API_KEY> <valor>'.")
|
|
151
|
+
set_env_key(name, value)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# --------------------------------------------------------------------------
|
|
155
|
+
# ações
|
|
156
|
+
# --------------------------------------------------------------------------
|
|
157
|
+
def _yes(b):
|
|
158
|
+
return common.c_green("ok") if b else common.c_red("faltando")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def show():
|
|
162
|
+
from . import llm
|
|
163
|
+
|
|
164
|
+
local = config.load_local()
|
|
165
|
+
b = llm.resolve_backend(config.BACKEND)
|
|
166
|
+
|
|
167
|
+
print(common.c_cyan("Integrações"))
|
|
168
|
+
if b == "claude_cli":
|
|
169
|
+
print(f" LLM claude_cli · comando `claude`: "
|
|
170
|
+
f"{_yes(bool(shutil.which('claude')))} (grátis, usa sua assinatura)")
|
|
171
|
+
elif b == "anthropic":
|
|
172
|
+
k = read_env_key("ANTHROPIC_API_KEY")
|
|
173
|
+
print(f" LLM anthropic · {config.CLAUDE_MODEL} · "
|
|
174
|
+
f"ANTHROPIC_API_KEY: {_yes(bool(k))}")
|
|
175
|
+
elif b == "ollama":
|
|
176
|
+
host = config.OLLAMA_HOST
|
|
177
|
+
up = False
|
|
178
|
+
try:
|
|
179
|
+
import urllib.request
|
|
180
|
+
urllib.request.urlopen(f"{host.rstrip('/')}/api/tags", timeout=3)
|
|
181
|
+
up = True
|
|
182
|
+
except Exception:
|
|
183
|
+
up = False
|
|
184
|
+
print(f" LLM ollama · {config.OLLAMA_MODEL} · {host}: "
|
|
185
|
+
f"{_yes(up)} (local, offline, grátis)")
|
|
186
|
+
elif b == "openai":
|
|
187
|
+
base = config.OPENAI_BASE_URL
|
|
188
|
+
local_srv = "localhost" in base or "127.0.0.1" in base
|
|
189
|
+
k = read_env_key("OPENAI_API_KEY")
|
|
190
|
+
print(f" LLM openai · {config.OPENAI_MODEL} · {base} · "
|
|
191
|
+
f"OPENAI_API_KEY: {_yes(bool(k) or local_srv)}")
|
|
192
|
+
elif b == "gemini":
|
|
193
|
+
k = read_env_key("GEMINI_API_KEY") or read_env_key("GOOGLE_API_KEY")
|
|
194
|
+
print(f" LLM gemini · {config.GEMINI_MODEL} · "
|
|
195
|
+
f"GEMINI_API_KEY: {_yes(bool(k))} (tier grátis no Google AI Studio)")
|
|
196
|
+
|
|
197
|
+
cap = common.check_capture_backend() or common.c_red("nenhum!")
|
|
198
|
+
print(f" Captura {cap} · monitor ativo: "
|
|
199
|
+
f"{_fmt(config.CAPTURE_ACTIVE_MONITOR_ONLY)}")
|
|
200
|
+
ns = "notify-send" if shutil.which("notify-send") else "plyer (fallback)"
|
|
201
|
+
print(f" Notificação {ns} · som: {_fmt(config.PLAY_SOUND)} · "
|
|
202
|
+
f"desktop: {_fmt(config.SHOW_NOTIFICATION)}")
|
|
203
|
+
|
|
204
|
+
print(common.c_cyan("\nConfiguração") +
|
|
205
|
+
common.c_dim(" (local sobrepõe o padrão)"))
|
|
206
|
+
for k in SETTABLE:
|
|
207
|
+
cur = getattr(config, k, None)
|
|
208
|
+
src = common.c_yellow("local") if k in local else common.c_dim("padrão")
|
|
209
|
+
line = f" {k.lower().replace('_', '-'):<28} {_fmt(cur):<14} {src}"
|
|
210
|
+
if k in local and config.default_of(k) is not None:
|
|
211
|
+
line += common.c_dim(f" (padrão: {_fmt(config.default_of(k))})")
|
|
212
|
+
print(line)
|
|
213
|
+
print(common.c_dim("\n wuptracker config set <chave> <valor> · "
|
|
214
|
+
"config unset <chave> · config api-key <chave>"))
|
|
215
|
+
print(common.c_dim(" wuptracker config helper → assistente interativo "
|
|
216
|
+
"(passo a passo, com explicações)"))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def set_(key, value):
|
|
220
|
+
kl = key.lower().replace("-", "_")
|
|
221
|
+
if kl in ("api_key", "apikey"):
|
|
222
|
+
set_api_key(value)
|
|
223
|
+
return
|
|
224
|
+
if kl.endswith("_api_key"): # ex: OPENAI_API_KEY
|
|
225
|
+
set_env_key(key.upper(), value)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
k = _resolve_key(key)
|
|
229
|
+
val = _coerce(k, value)
|
|
230
|
+
config.set_local(k, val)
|
|
231
|
+
print(common.c_green(f"✓ {k} = {_fmt(val)}") +
|
|
232
|
+
common.c_dim(f" → {config.CONFIG_LOCAL_PATH}"))
|
|
233
|
+
|
|
234
|
+
if k == "BACKEND":
|
|
235
|
+
from . import llm
|
|
236
|
+
|
|
237
|
+
b = llm.resolve_backend(val)
|
|
238
|
+
if b == "claude_cli" and not shutil.which("claude"):
|
|
239
|
+
print(common.c_yellow(" ⚠ comando `claude` não encontrado no PATH"))
|
|
240
|
+
if b == "anthropic" and not read_env_key("ANTHROPIC_API_KEY"):
|
|
241
|
+
print(common.c_yellow(" ⚠ falta a chave: wuptracker config api-key <chave>"))
|
|
242
|
+
if b == "openai" and not read_env_key("OPENAI_API_KEY") \
|
|
243
|
+
and "localhost" not in config.OPENAI_BASE_URL:
|
|
244
|
+
print(common.c_yellow(" ⚠ falta a chave: wuptracker config api-key <chave> "
|
|
245
|
+
"(ou aponte openai-url para um servidor local)"))
|
|
246
|
+
if b == "ollama":
|
|
247
|
+
print(common.c_dim(" → 'wuptracker config set ollama-model <modelo-com-visão>' "
|
|
248
|
+
"· 'ollama pull <modelo>'"))
|
|
249
|
+
if b == "gemini" and not (read_env_key("GEMINI_API_KEY")
|
|
250
|
+
or read_env_key("GOOGLE_API_KEY")):
|
|
251
|
+
print(common.c_yellow(" ⚠ falta a chave (grátis em "
|
|
252
|
+
"aistudio.google.com/apikey): "
|
|
253
|
+
"wuptracker config api-key <chave>"))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def unset_(key):
|
|
257
|
+
k = _resolve_key(key)
|
|
258
|
+
config.unset_local(k)
|
|
259
|
+
print(common.c_green(f"✓ {k} voltou ao padrão ({_fmt(config.default_of(k))})"))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# --------------------------------------------------------------------------
|
|
263
|
+
# wizard interativo — `wuptracker config helper`
|
|
264
|
+
# --------------------------------------------------------------------------
|
|
265
|
+
BACKEND_MORE = """
|
|
266
|
+
claude_cli Usa o CLI do Claude Code (comando `claude`). Consome a cota da sua
|
|
267
|
+
assinatura Pro/Max — zero custo por token, zero API key. As imagens
|
|
268
|
+
sobem para a Anthropic. É o padrão, e o mais simples de começar.
|
|
269
|
+
|
|
270
|
+
gemini API do Google Gemini. Tem um tier gratuito generoso, sem cartão —
|
|
271
|
+
pegue a chave em https://aistudio.google.com/apikey. Boa opção grátis
|
|
272
|
+
se você não usa Claude Code. As imagens sobem para o Google.
|
|
273
|
+
|
|
274
|
+
ollama Roda um modelo NA SUA máquina via Ollama. Nada sai do computador —
|
|
275
|
+
ideal para engajamentos onde as telas são confidenciais. Precisa de
|
|
276
|
+
um modelo com visão baixado (ex.: `ollama pull llama3.2-vision`).
|
|
277
|
+
Mais lento e um pouco menos preciso que os modelos de nuvem.
|
|
278
|
+
|
|
279
|
+
openai Qualquer endpoint no formato da API OpenAI: OpenAI, Groq (rápido,
|
|
280
|
+
tem tier grátis), OpenRouter (vários modelos), LM Studio / llama.cpp
|
|
281
|
+
(local). Você escolhe a URL, o modelo e a chave.
|
|
282
|
+
|
|
283
|
+
anthropic API paga da Anthropic, cobrada por token — use se quiser os modelos
|
|
284
|
+
Claude sem depender do CLI/assinatura.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
PROFILE_MORE = """
|
|
288
|
+
thm Para pentest e CTF (TryHackMe, HTB...). Seções clássicas:
|
|
289
|
+
Reconhecimento, Enumeração, Acesso Inicial, Pós-Exploração,
|
|
290
|
+
Escalação de Privilégio, Flags, Lições Aprendidas. A análise de cada
|
|
291
|
+
tela foca em portas, serviços, comandos, credenciais e flags.
|
|
292
|
+
|
|
293
|
+
generico Para qualquer outra coisa: dev, debugging, pesquisa, configuração,
|
|
294
|
+
estudo. O writeup é um registro cronológico: Resumo, Linha do Tempo,
|
|
295
|
+
Descobertas, Problemas Encontrados, Estado Final. Não força jargão
|
|
296
|
+
de pentest onde não existe.
|
|
297
|
+
|
|
298
|
+
Dá pra trocar por sessão: wuptracker capture "Nome" --profile generico
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
STYLE_MORE = """
|
|
302
|
+
tecnico O mais completo: Markdown com frontmatter, seções fixas do perfil e
|
|
303
|
+
âncoras de horário [HH:MM:SS]. Bloco de código pra todo comando.
|
|
304
|
+
O formato "documentação". Embute as imagens.
|
|
305
|
+
|
|
306
|
+
corrido A mesma informação, em prosa — narrativa fluida, sem tabelas nem
|
|
307
|
+
lista de fases. Bom pra publicar como relato. Embute as imagens.
|
|
308
|
+
|
|
309
|
+
resumo Curto (~250 palavras): 1 parágrafo de contexto + bullets do que foi
|
|
310
|
+
feito/descoberto + conclusão. Sem imagens. Pra colar num chat/e-mail.
|
|
311
|
+
|
|
312
|
+
blog Artigo didático estilo dev.to/Medium: introdução que situa o leitor,
|
|
313
|
+
seções temáticas, código comentado, "o que aprendi". Embute imagens.
|
|
314
|
+
|
|
315
|
+
linkedin Post pronto pra publicar: 1ª pessoa, gancho na 1ª linha, bullets de
|
|
316
|
+
aprendizado, hashtags. Texto puro (sem markdown) e GENERALIZA dados
|
|
317
|
+
sensíveis (IPs, flags, nomes de cliente). Sem imagens.
|
|
318
|
+
|
|
319
|
+
Na geração dá pra pedir vários de uma vez:
|
|
320
|
+
wuptracker generate 1 --style tecnico,linkedin
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _hr():
|
|
325
|
+
print(common.c_dim("─" * 62))
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _ask(question, default=""):
|
|
329
|
+
hint = f" [{default}]" if default else ""
|
|
330
|
+
try:
|
|
331
|
+
r = input(common.c_cyan(f"› {question}{hint} ")).strip()
|
|
332
|
+
except EOFError:
|
|
333
|
+
r = ""
|
|
334
|
+
return r or default
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _ask_yesno(question, default=True):
|
|
338
|
+
hint = "S/n" if default else "s/N"
|
|
339
|
+
r = _ask(f"{question} ({hint})").strip().lower()
|
|
340
|
+
if not r:
|
|
341
|
+
return default
|
|
342
|
+
return r in ("s", "sim", "y", "yes", "1", "on")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _ask_choice(question, options, more_text=None):
|
|
346
|
+
"""options: [(valor, descrição curta), ...]. '?' mostra more_text."""
|
|
347
|
+
while True:
|
|
348
|
+
print(common.c_cyan(f"\n{question}"))
|
|
349
|
+
for i, (val, desc) in enumerate(options, 1):
|
|
350
|
+
print(f" {i}) {common.c_green(val)} — {desc}")
|
|
351
|
+
if more_text:
|
|
352
|
+
print(f" {common.c_dim('?) saiba mais sobre cada opção')}")
|
|
353
|
+
r = _ask("escolha", "1")
|
|
354
|
+
if r == "?" and more_text:
|
|
355
|
+
_hr()
|
|
356
|
+
print(more_text.strip("\n"))
|
|
357
|
+
_hr()
|
|
358
|
+
continue
|
|
359
|
+
if r.isdigit() and 1 <= int(r) <= len(options):
|
|
360
|
+
return options[int(r) - 1][0]
|
|
361
|
+
for val, _ in options:
|
|
362
|
+
if r.lower() == val.lower():
|
|
363
|
+
return val
|
|
364
|
+
print(common.c_red(" opção inválida, tente de novo"))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _ask_model(question, presets, current, note=None):
|
|
368
|
+
"""Menu de modelos sugeridos + opção de digitar outro nome.
|
|
369
|
+
|
|
370
|
+
presets: lista de strings. `current` (se não estiver em presets) aparece
|
|
371
|
+
marcado como o valor em uso. Retorna a string escolhida/digitada."""
|
|
372
|
+
options = list(presets)
|
|
373
|
+
if current and current not in options:
|
|
374
|
+
options = [current] + options
|
|
375
|
+
print(common.c_cyan(f"\n{question}"))
|
|
376
|
+
if note:
|
|
377
|
+
print(common.c_dim(f" {note}"))
|
|
378
|
+
for i, m in enumerate(options, 1):
|
|
379
|
+
tag = common.c_dim(" (em uso)") if m == current else ""
|
|
380
|
+
print(f" {i}) {m}{tag}")
|
|
381
|
+
other_idx = len(options) + 1
|
|
382
|
+
print(f" {other_idx}) outro… (digitar o nome do modelo)")
|
|
383
|
+
|
|
384
|
+
default_idx = str(options.index(current) + 1) if current in options else "1"
|
|
385
|
+
while True:
|
|
386
|
+
r = _ask("escolha", default_idx)
|
|
387
|
+
if r.isdigit():
|
|
388
|
+
n = int(r)
|
|
389
|
+
if 1 <= n <= len(options):
|
|
390
|
+
return options[n - 1]
|
|
391
|
+
if n == other_idx:
|
|
392
|
+
return _ask("Nome do modelo", current or "")
|
|
393
|
+
elif r: # também aceita digitar o nome direto
|
|
394
|
+
return r
|
|
395
|
+
print(common.c_red(" opção inválida, tente de novo"))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _set_if_changed(key, value):
|
|
399
|
+
if value == config.default_of(key):
|
|
400
|
+
config.unset_local(key)
|
|
401
|
+
else:
|
|
402
|
+
config.set_local(key, value)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def wizard():
|
|
406
|
+
"""`wuptracker config helper` — assistente interativo de configuração."""
|
|
407
|
+
print(common.c_cyan("╔══════════════════════════════════════════════╗"))
|
|
408
|
+
print(common.c_cyan("║ wuptracker — assistente de setup ║"))
|
|
409
|
+
print(common.c_cyan("╚══════════════════════════════════════════════╝"))
|
|
410
|
+
print("Enter aceita o valor entre [colchetes]. Digite " +
|
|
411
|
+
common.c_dim("?") + " numa pergunta de múltipla escolha para saber mais.")
|
|
412
|
+
|
|
413
|
+
cur = config
|
|
414
|
+
|
|
415
|
+
# 1. Backend de IA -----------------------------------------------------
|
|
416
|
+
backend = _ask_choice(
|
|
417
|
+
"Qual IA vai analisar as telas e escrever os writeups?",
|
|
418
|
+
[("claude_cli", "grátis, usa sua assinatura do Claude Code"),
|
|
419
|
+
("gemini", "Google, tier gratuito generoso (precisa de key grátis)"),
|
|
420
|
+
("ollama", "100% local e offline — nada sai da sua máquina"),
|
|
421
|
+
("openai", "OpenAI / Groq / OpenRouter / LM Studio…"),
|
|
422
|
+
("anthropic", "API paga da Anthropic")],
|
|
423
|
+
BACKEND_MORE)
|
|
424
|
+
_set_if_changed("BACKEND", backend)
|
|
425
|
+
|
|
426
|
+
if backend == "claude_cli":
|
|
427
|
+
if shutil.which("claude"):
|
|
428
|
+
print(common.c_green(" ✓ comando 'claude' encontrado no PATH"))
|
|
429
|
+
else:
|
|
430
|
+
print(common.c_yellow(" ⚠ 'claude' não encontrado — instale o Claude Code"))
|
|
431
|
+
model = _ask_model(
|
|
432
|
+
"Modelo do CLI",
|
|
433
|
+
["padrão do CLI", "opus", "sonnet", "haiku"],
|
|
434
|
+
cur.CLAUDE_CLI_MODEL or "padrão do CLI")
|
|
435
|
+
_set_if_changed("CLAUDE_CLI_MODEL",
|
|
436
|
+
None if model == "padrão do CLI" else model)
|
|
437
|
+
|
|
438
|
+
elif backend == "gemini":
|
|
439
|
+
print(common.c_dim(" Chave grátis em https://aistudio.google.com/apikey"))
|
|
440
|
+
k = _ask("GEMINI_API_KEY (Enter para configurar depois)")
|
|
441
|
+
if k:
|
|
442
|
+
set_env_key("GEMINI_API_KEY", k)
|
|
443
|
+
model = _ask_model(
|
|
444
|
+
"Modelo Gemini",
|
|
445
|
+
["gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"],
|
|
446
|
+
cur.GEMINI_MODEL)
|
|
447
|
+
_set_if_changed("GEMINI_MODEL", model)
|
|
448
|
+
|
|
449
|
+
elif backend == "ollama":
|
|
450
|
+
host = _ask("URL do Ollama", cur.OLLAMA_HOST)
|
|
451
|
+
_set_if_changed("OLLAMA_HOST", host)
|
|
452
|
+
model = _ask_model(
|
|
453
|
+
"Modelo (precisa ter visão)",
|
|
454
|
+
["llama3.2-vision", "llava", "qwen2.5vl", "minicpm-v", "moondream"],
|
|
455
|
+
cur.OLLAMA_MODEL,
|
|
456
|
+
note="se ainda não tiver: ollama pull <modelo>")
|
|
457
|
+
_set_if_changed("OLLAMA_MODEL", model)
|
|
458
|
+
try:
|
|
459
|
+
import json as _j
|
|
460
|
+
import urllib.request
|
|
461
|
+
|
|
462
|
+
with urllib.request.urlopen(f"{host.rstrip('/')}/api/tags",
|
|
463
|
+
timeout=3) as r:
|
|
464
|
+
names = {m2.get("name", "").split(":")[0]
|
|
465
|
+
for m2 in _j.loads(r.read()).get("models", [])}
|
|
466
|
+
if model.split(":")[0] in names:
|
|
467
|
+
print(common.c_green(f" ✓ Ollama respondeu e '{model}' está baixado"))
|
|
468
|
+
else:
|
|
469
|
+
print(common.c_yellow(f" ⚠ Ollama respondeu, mas '{model}' não está "
|
|
470
|
+
f"baixado — rode: ollama pull {model}"))
|
|
471
|
+
except Exception:
|
|
472
|
+
print(common.c_yellow(f" ⚠ Ollama não respondeu em {host} — "
|
|
473
|
+
"rode 'ollama serve' antes de usar"))
|
|
474
|
+
|
|
475
|
+
elif backend == "openai":
|
|
476
|
+
provider = _ask_choice(
|
|
477
|
+
"Qual endpoint compatível com a API OpenAI?",
|
|
478
|
+
[("openai", "api.openai.com — modelos GPT oficiais"),
|
|
479
|
+
("groq", "api.groq.com — bem rápido, tem tier grátis"),
|
|
480
|
+
("openrouter", "openrouter.ai — vários modelos, um endpoint só"),
|
|
481
|
+
("local", "LM Studio / llama.cpp / vLLM na sua máquina"),
|
|
482
|
+
("outro", "digitar a URL manualmente")])
|
|
483
|
+
presets = {
|
|
484
|
+
"openai": ("https://api.openai.com/v1", ["gpt-4o-mini", "gpt-4o"]),
|
|
485
|
+
"groq": ("https://api.groq.com/openai/v1",
|
|
486
|
+
["llama-3.2-90b-vision-preview",
|
|
487
|
+
"llama-3.2-11b-vision-preview"]),
|
|
488
|
+
"openrouter": ("https://openrouter.ai/api/v1",
|
|
489
|
+
["google/gemini-2.0-flash-001", "openai/gpt-4o-mini"]),
|
|
490
|
+
"local": ("http://localhost:1234/v1", ["local-model"]),
|
|
491
|
+
}
|
|
492
|
+
if provider == "outro":
|
|
493
|
+
url = _ask("URL do endpoint (OPENAI_BASE_URL)", cur.OPENAI_BASE_URL)
|
|
494
|
+
model_presets = [cur.OPENAI_MODEL] if cur.OPENAI_MODEL else []
|
|
495
|
+
else:
|
|
496
|
+
url, model_presets = presets[provider]
|
|
497
|
+
_set_if_changed("OPENAI_BASE_URL", url)
|
|
498
|
+
model = _ask_model("Modelo", model_presets, cur.OPENAI_MODEL)
|
|
499
|
+
_set_if_changed("OPENAI_MODEL", model)
|
|
500
|
+
|
|
501
|
+
local_srv = "localhost" in url or "127.0.0.1" in url
|
|
502
|
+
prompt = "OPENAI_API_KEY" + (" (Enter para pular — endpoint local)"
|
|
503
|
+
if local_srv else " (Enter p/ configurar depois)")
|
|
504
|
+
k = _ask(prompt)
|
|
505
|
+
if k:
|
|
506
|
+
set_env_key("OPENAI_API_KEY", k)
|
|
507
|
+
|
|
508
|
+
elif backend == "anthropic":
|
|
509
|
+
k = _ask("ANTHROPIC_API_KEY (Enter para configurar depois)")
|
|
510
|
+
if k:
|
|
511
|
+
set_env_key("ANTHROPIC_API_KEY", k)
|
|
512
|
+
model = _ask_model(
|
|
513
|
+
"Modelo",
|
|
514
|
+
["claude-sonnet-5", "claude-opus-5", "claude-haiku-4-5-20251001"],
|
|
515
|
+
cur.CLAUDE_MODEL)
|
|
516
|
+
_set_if_changed("CLAUDE_MODEL", model)
|
|
517
|
+
|
|
518
|
+
# 2. Perfil --------------------------------------------------------------
|
|
519
|
+
profile = _ask_choice(
|
|
520
|
+
"Perfil padrão dos writeups (dá pra trocar por sessão)?",
|
|
521
|
+
[("thm", "pentest / CTF — fases, flags, escalação de privilégio"),
|
|
522
|
+
("generico", "qualquer trabalho no PC — linha do tempo")],
|
|
523
|
+
PROFILE_MORE)
|
|
524
|
+
_set_if_changed("DEFAULT_PROFILE", profile)
|
|
525
|
+
|
|
526
|
+
# 3. Estilo ----------------------------------------------------------------
|
|
527
|
+
style = _ask_choice(
|
|
528
|
+
"Estilo padrão do writeup (dá pra pedir vários na hora de gerar)?",
|
|
529
|
+
[("tecnico", "denso, seções fixas, com horários — o mais completo"),
|
|
530
|
+
("corrido", "narrativa em texto corrido"),
|
|
531
|
+
("resumo", "resumo executivo curto, sem imagens"),
|
|
532
|
+
("blog", "artigo didático estilo dev.to/Medium"),
|
|
533
|
+
("linkedin", "post pronto pro LinkedIn, texto puro")],
|
|
534
|
+
STYLE_MORE)
|
|
535
|
+
_set_if_changed("DEFAULT_STYLE", style)
|
|
536
|
+
|
|
537
|
+
# 4. Preferências rápidas --------------------------------------------------
|
|
538
|
+
print(common.c_cyan("\nUns últimos ajustes rápidos:"))
|
|
539
|
+
v = _ask_yesno(" Capturar só o monitor sob o cursor? (você tem +1 monitor?)",
|
|
540
|
+
cur.CAPTURE_ACTIVE_MONITOR_ONLY)
|
|
541
|
+
_set_if_changed("CAPTURE_ACTIVE_MONITOR_ONLY", v)
|
|
542
|
+
|
|
543
|
+
v = _ask_yesno(" Otimizar as imagens do writeup pra WebP (bem mais leve)?",
|
|
544
|
+
cur.WEBP_IMAGES)
|
|
545
|
+
_set_if_changed("WEBP_IMAGES", v)
|
|
546
|
+
|
|
547
|
+
v = _ask_yesno(" Notificação de desktop a cada captura (F9)?",
|
|
548
|
+
cur.SHOW_NOTIFICATION)
|
|
549
|
+
_set_if_changed("SHOW_NOTIFICATION", v)
|
|
550
|
+
|
|
551
|
+
# 5. Resultado -------------------------------------------------------------
|
|
552
|
+
print(common.c_green("\n✓ Configuração salva.\n"))
|
|
553
|
+
show()
|
|
554
|
+
try:
|
|
555
|
+
from . import llm
|
|
556
|
+
|
|
557
|
+
llm.preflight()
|
|
558
|
+
print(common.c_green("\n✓ Tudo pronto — rode 'wuptracker capture \"Nome\"' "
|
|
559
|
+
"pra começar."))
|
|
560
|
+
except SystemExit as e:
|
|
561
|
+
print(common.c_yellow(f"\n{e}"))
|