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/__init__.py
ADDED
wuptracker/capture.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Captura de tela em background durante uma sessão de trabalho.
|
|
2
|
+
|
|
3
|
+
Uso:
|
|
4
|
+
python capture.py "Skynet"
|
|
5
|
+
python capture.py "Refatorar API" --profile generico
|
|
6
|
+
python capture.py # pede o nome interativamente
|
|
7
|
+
|
|
8
|
+
Perfis (--profile): "thm" (pentest/CTF) | "generico" (qualquer trabalho).
|
|
9
|
+
Padrão em config.DEFAULT_PROFILE.
|
|
10
|
+
|
|
11
|
+
Durante a sessão:
|
|
12
|
+
F9 -> captura a tela, analisa com o Claude e salva
|
|
13
|
+
F10 / Ctrl+C -> encerra a sessão
|
|
14
|
+
(Wayland) pkill -USR1/-USR2 -f capture.py
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import datetime as dt
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import signal
|
|
21
|
+
import sys
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
25
|
+
|
|
26
|
+
from pynput import keyboard
|
|
27
|
+
|
|
28
|
+
from . import common
|
|
29
|
+
from . import config
|
|
30
|
+
from . import llm
|
|
31
|
+
from . import profiles
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Session:
|
|
35
|
+
def __init__(self, room, profile="thm"):
|
|
36
|
+
self.room = room
|
|
37
|
+
self.profile = profiles.resolve(profile)
|
|
38
|
+
self.analyze_prompt = profiles.get(self.profile)["analyze_prompt"]
|
|
39
|
+
self.start = dt.datetime.now()
|
|
40
|
+
stamp = self.start.strftime("%Y-%m-%d_%H-%M")
|
|
41
|
+
self.name = f"{stamp}_{common.slugify(room)}"
|
|
42
|
+
self.dir = os.path.join(config.SESSIONS_DIR, self.name)
|
|
43
|
+
self.captures_dir = os.path.join(self.dir, "captures")
|
|
44
|
+
os.makedirs(self.captures_dir, exist_ok=True)
|
|
45
|
+
self.captures = [] # entradas do session.json
|
|
46
|
+
self.lock = threading.Lock()
|
|
47
|
+
self.count = 0 # nº de capturas que começaram a processar
|
|
48
|
+
self.submitted = 0 # nº de F9/sinais disparados
|
|
49
|
+
self.completed = 0 # nº de capturas que terminaram (ok ou erro)
|
|
50
|
+
self._write_meta()
|
|
51
|
+
self._write_session()
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def pending(self):
|
|
55
|
+
return self.submitted - self.completed
|
|
56
|
+
|
|
57
|
+
def submit(self, pool):
|
|
58
|
+
with self.lock:
|
|
59
|
+
self.submitted += 1
|
|
60
|
+
pool.submit(self._run)
|
|
61
|
+
|
|
62
|
+
def _run(self):
|
|
63
|
+
try:
|
|
64
|
+
self.do_capture()
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(common.c_red(f"[!] erro na captura: {e}"))
|
|
67
|
+
finally:
|
|
68
|
+
with self.lock:
|
|
69
|
+
self.completed += 1
|
|
70
|
+
|
|
71
|
+
# ---------- persistência ----------
|
|
72
|
+
def _write_meta(self, ended=None):
|
|
73
|
+
meta = {
|
|
74
|
+
"room": self.room,
|
|
75
|
+
"profile": self.profile,
|
|
76
|
+
"started_at": self.start.isoformat(timespec="seconds"),
|
|
77
|
+
"system": common.system_name(),
|
|
78
|
+
}
|
|
79
|
+
if ended:
|
|
80
|
+
meta["ended_at"] = ended.isoformat(timespec="seconds")
|
|
81
|
+
meta["duration"] = common.fmt_elapsed(
|
|
82
|
+
(ended - self.start).total_seconds())
|
|
83
|
+
with open(os.path.join(self.dir, "meta.json"), "w", encoding="utf-8") as f:
|
|
84
|
+
json.dump(meta, f, indent=2, ensure_ascii=False)
|
|
85
|
+
|
|
86
|
+
def _write_session(self):
|
|
87
|
+
ordered = sorted(self.captures, key=lambda c: c["filename"])
|
|
88
|
+
with open(os.path.join(self.dir, "session.json"), "w", encoding="utf-8") as f:
|
|
89
|
+
json.dump({"captures": ordered}, f, indent=2, ensure_ascii=False)
|
|
90
|
+
|
|
91
|
+
def add_capture(self, entry):
|
|
92
|
+
with self.lock:
|
|
93
|
+
self.captures.append(entry)
|
|
94
|
+
self._write_session()
|
|
95
|
+
|
|
96
|
+
# ---------- captura ----------
|
|
97
|
+
def elapsed_label(self):
|
|
98
|
+
secs = (dt.datetime.now() - self.start).total_seconds()
|
|
99
|
+
return common.fmt_elapsed(secs)
|
|
100
|
+
|
|
101
|
+
def do_capture(self):
|
|
102
|
+
label = self.elapsed_label()
|
|
103
|
+
fname = label.replace(":", "-")
|
|
104
|
+
png_path = os.path.join(self.captures_dir, f"{fname}.png")
|
|
105
|
+
json_path = os.path.join(self.captures_dir, f"{fname}.json")
|
|
106
|
+
|
|
107
|
+
with self.lock:
|
|
108
|
+
self.count += 1
|
|
109
|
+
n = self.count
|
|
110
|
+
queued = self.pending - 1
|
|
111
|
+
q = f" ({queued} na fila)" if queued > 0 else ""
|
|
112
|
+
print(common.c_yellow(f"[{label}] capturando… (#{n}){q}"))
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
common.grab_screen(png_path)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(common.c_red(f"[{label}] falha na captura de tela: {e}"))
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
common.beep()
|
|
121
|
+
|
|
122
|
+
analysis = self._analyze(png_path, label)
|
|
123
|
+
|
|
124
|
+
# imagem a exibir no writeup: recorte quando o modelo indicar uma região
|
|
125
|
+
image = f"{fname}.png"
|
|
126
|
+
crop_note = "sem recorte"
|
|
127
|
+
if getattr(config, "CROP_IMAGES", True) and isinstance(analysis.get("crop"), dict):
|
|
128
|
+
crop_path = os.path.join(self.captures_dir, f"{fname}_crop.png")
|
|
129
|
+
try:
|
|
130
|
+
if common.crop_image(png_path, crop_path, analysis["crop"]):
|
|
131
|
+
image = f"{fname}_crop.png"
|
|
132
|
+
ow, oh = common.image_size(png_path)
|
|
133
|
+
cw, ch = common.image_size(crop_path)
|
|
134
|
+
pct = round(100 * (cw * ch) / (ow * oh)) if ow and oh else 0
|
|
135
|
+
crop_note = f"recorte {cw}×{ch} ({pct}% de {ow}×{oh})"
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(common.c_yellow(f"[{label}] recorte falhou: {e}"))
|
|
138
|
+
analysis["image"] = image
|
|
139
|
+
|
|
140
|
+
with open(json_path, "w", encoding="utf-8") as f:
|
|
141
|
+
json.dump(analysis, f, indent=2, ensure_ascii=False)
|
|
142
|
+
|
|
143
|
+
title = analysis.get("title") or "[análise pendente]"
|
|
144
|
+
# "phase" (thm) ou "category" (generico) — guarda o que houver
|
|
145
|
+
bucket = analysis.get("phase") or analysis.get("category") or "misc"
|
|
146
|
+
self.add_capture({
|
|
147
|
+
"timestamp": label,
|
|
148
|
+
"filename": fname,
|
|
149
|
+
"title": title,
|
|
150
|
+
"phase": bucket,
|
|
151
|
+
"image": image,
|
|
152
|
+
"is_key_moment": bool(analysis.get("is_key_moment")),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
star = " ★" if analysis.get("is_key_moment") else ""
|
|
156
|
+
print(common.c_green(f"[{label}] ✓ {title}{star}") +
|
|
157
|
+
common.c_dim(f" · {crop_note}"))
|
|
158
|
+
common.notify("Writeup Capture", f"[{label}] {title}")
|
|
159
|
+
|
|
160
|
+
def _analyze(self, png_path, label):
|
|
161
|
+
try:
|
|
162
|
+
text = llm.analyze_image(png_path, self.analyze_prompt)
|
|
163
|
+
return _parse_json(text)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(common.c_red(f"[{label}] análise falhou: {e}"))
|
|
166
|
+
return {
|
|
167
|
+
"error": str(e),
|
|
168
|
+
"description": "[análise pendente]",
|
|
169
|
+
"phase": "misc",
|
|
170
|
+
"title": f"Captura {label} (análise pendente)",
|
|
171
|
+
"commands": [], "findings": [], "tool": None,
|
|
172
|
+
"is_key_moment": False, "key_moment_reason": None,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
def finish(self):
|
|
176
|
+
end = dt.datetime.now()
|
|
177
|
+
self._write_meta(ended=end)
|
|
178
|
+
try:
|
|
179
|
+
os.remove(os.path.join(self.dir, "capture.pid"))
|
|
180
|
+
except OSError:
|
|
181
|
+
pass
|
|
182
|
+
dur = common.fmt_elapsed((end - self.start).total_seconds())
|
|
183
|
+
done = len(self.captures)
|
|
184
|
+
print()
|
|
185
|
+
print(common.c_cyan(f"Sessão encerrada. {done} capturas em {dur}"))
|
|
186
|
+
if done != self.count:
|
|
187
|
+
print(common.c_yellow(
|
|
188
|
+
f"({self.count - done} captura(s) não finalizaram a análise)"))
|
|
189
|
+
print(common.c_dim(f"Gerar o writeup: wuptracker generate {self.name}"))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _parse_json(text):
|
|
193
|
+
try:
|
|
194
|
+
return json.loads(text)
|
|
195
|
+
except json.JSONDecodeError:
|
|
196
|
+
start = text.find("{")
|
|
197
|
+
end = text.rfind("}")
|
|
198
|
+
if start != -1 and end != -1:
|
|
199
|
+
try:
|
|
200
|
+
return json.loads(text[start:end + 1])
|
|
201
|
+
except json.JSONDecodeError:
|
|
202
|
+
pass
|
|
203
|
+
return {
|
|
204
|
+
"error": "resposta não-JSON",
|
|
205
|
+
"raw": text,
|
|
206
|
+
"description": "[análise pendente]",
|
|
207
|
+
"phase": "misc", "title": "[análise pendente]",
|
|
208
|
+
"commands": [], "findings": [], "tool": None,
|
|
209
|
+
"is_key_moment": False, "key_moment_reason": None,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def capture(room=None, profile=None):
|
|
214
|
+
"""Inicia o modo de captura e bloqueia até o encerramento da sessão."""
|
|
215
|
+
try:
|
|
216
|
+
sys.stdout.reconfigure(line_buffering=True)
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
backend = common.check_capture_backend()
|
|
220
|
+
if not backend:
|
|
221
|
+
sys.exit("[!] Nenhum backend de captura disponível. Instale 'mss' "
|
|
222
|
+
"(pip install -r requirements.txt) ou 'scrot'.")
|
|
223
|
+
# valida o backend do LLM cedo
|
|
224
|
+
llm.preflight()
|
|
225
|
+
|
|
226
|
+
profile = profiles.resolve_domain(
|
|
227
|
+
profile or getattr(config, "DEFAULT_PROFILE", "thm"))
|
|
228
|
+
|
|
229
|
+
room = room or input("Nome da sessão: ").strip()
|
|
230
|
+
if not room:
|
|
231
|
+
sys.exit("[!] Nome da sessão vazio.")
|
|
232
|
+
|
|
233
|
+
session = Session(room, profile)
|
|
234
|
+
pool = ThreadPoolExecutor(max_workers=4)
|
|
235
|
+
stop = threading.Event()
|
|
236
|
+
|
|
237
|
+
# SIGUSR1 = capturar | SIGUSR2 = encerrar (para atalhos globais no Wayland).
|
|
238
|
+
signal.signal(signal.SIGUSR1, lambda *_: session.submit(pool))
|
|
239
|
+
signal.signal(signal.SIGUSR2, lambda *_: stop.set())
|
|
240
|
+
|
|
241
|
+
pid = os.getpid()
|
|
242
|
+
prog = os.path.basename(sys.argv[0]) or "wuptracker.py"
|
|
243
|
+
pidfile = os.path.join(session.dir, "capture.pid")
|
|
244
|
+
try:
|
|
245
|
+
with open(pidfile, "w") as f:
|
|
246
|
+
f.write(str(pid))
|
|
247
|
+
except OSError:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
print(common.c_green(f"[✓] Sessão iniciada: {session.name}"))
|
|
251
|
+
print(f" Perfil: {session.profile} | Captura: {backend} | PID: {pid}")
|
|
252
|
+
|
|
253
|
+
wayland = bool(os.environ.get("WAYLAND_DISPLAY")) or \
|
|
254
|
+
os.environ.get("XDG_SESSION_TYPE", "").lower() == "wayland"
|
|
255
|
+
|
|
256
|
+
listener = None
|
|
257
|
+
if wayland:
|
|
258
|
+
print(common.c_yellow(
|
|
259
|
+
" Wayland: teclas globais (F9) podem não funcionar. Use sinais —\n"
|
|
260
|
+
f" capturar: kill -USR1 {pid} (ou pkill -USR1 -f {prog})\n"
|
|
261
|
+
f" encerrar: kill -USR2 {pid} (ou pkill -USR2 -f {prog})\n"
|
|
262
|
+
" Dica: crie um atalho global (KDE/GNOME) apontando para esse comando.\n"
|
|
263
|
+
" (ou mantenha este terminal focado para o F9 funcionar)"))
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
capture_key = getattr(keyboard.Key, config.CAPTURE_KEY)
|
|
267
|
+
exit_key = getattr(keyboard.Key, config.EXIT_KEY)
|
|
268
|
+
|
|
269
|
+
def on_press(key):
|
|
270
|
+
if key == capture_key:
|
|
271
|
+
session.submit(pool)
|
|
272
|
+
elif key == exit_key:
|
|
273
|
+
stop.set()
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
listener = keyboard.Listener(on_press=on_press)
|
|
277
|
+
listener.start()
|
|
278
|
+
print(f" F9 = capturar | F10 ou Ctrl+C = encerrar")
|
|
279
|
+
except Exception as e:
|
|
280
|
+
print(common.c_yellow(f" Listener de teclado indisponível ({e}). "
|
|
281
|
+
"Use os sinais acima ou Ctrl+C."))
|
|
282
|
+
|
|
283
|
+
last_note = 0.0
|
|
284
|
+
try:
|
|
285
|
+
while not stop.is_set():
|
|
286
|
+
time.sleep(0.2)
|
|
287
|
+
now = time.time()
|
|
288
|
+
if session.pending and now - last_note > 8:
|
|
289
|
+
print(common.c_dim(
|
|
290
|
+
f" … {session.pending} análise(s) em processamento"))
|
|
291
|
+
last_note = now
|
|
292
|
+
except KeyboardInterrupt:
|
|
293
|
+
print()
|
|
294
|
+
finally:
|
|
295
|
+
if listener is not None:
|
|
296
|
+
listener.stop()
|
|
297
|
+
_drain(session, pool)
|
|
298
|
+
session.finish()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _drain(session, pool):
|
|
302
|
+
"""Espera as análises pendentes terminarem, com progresso e proteção contra
|
|
303
|
+
um segundo Ctrl+C que deixaria arquivos pela metade."""
|
|
304
|
+
if session.pending <= 0:
|
|
305
|
+
pool.shutdown(wait=True)
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
print(common.c_yellow(
|
|
309
|
+
f"⏳ {session.pending} análise(s) ainda processando — "
|
|
310
|
+
"NÃO feche o terminal."))
|
|
311
|
+
forced = False
|
|
312
|
+
try:
|
|
313
|
+
while session.completed < session.submitted:
|
|
314
|
+
done, total = session.completed, session.submitted
|
|
315
|
+
print(f"\r {done}/{total} concluídas… ", end="", flush=True)
|
|
316
|
+
time.sleep(0.3)
|
|
317
|
+
print(f"\r {session.submitted}/{session.submitted} concluídas. ")
|
|
318
|
+
except KeyboardInterrupt:
|
|
319
|
+
forced = True
|
|
320
|
+
left = session.submitted - session.completed
|
|
321
|
+
print(common.c_red(
|
|
322
|
+
f"\n[!] Interrompido à força com {left} análise(s) incompleta(s). "
|
|
323
|
+
"Os screenshots (.png) foram salvos; as análises faltantes ficarão "
|
|
324
|
+
"sem .json. O generate.py ainda funciona com o que houver."))
|
|
325
|
+
pool.shutdown(wait=not forced, cancel_futures=forced)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def main(argv=None):
|
|
329
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
330
|
+
profile = None
|
|
331
|
+
if "--profile" in args:
|
|
332
|
+
i = args.index("--profile")
|
|
333
|
+
try:
|
|
334
|
+
profile = args[i + 1]
|
|
335
|
+
except IndexError:
|
|
336
|
+
sys.exit("[!] --profile requer um valor (thm | generico).")
|
|
337
|
+
del args[i:i + 2]
|
|
338
|
+
capture(args[0] if args else None, profile)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
main()
|
wuptracker/cli.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""wuptracker — captura e gera writeups de sessões de trabalho / pentest.
|
|
2
|
+
|
|
3
|
+
wuptracker capture ["Nome da sessão"] [--profile thm|generico]
|
|
4
|
+
wuptracker show [--writeups]
|
|
5
|
+
wuptracker generate [SESSÃO] [--style ...] [--profile ...] [--export DIR] [--open]
|
|
6
|
+
wuptracker styles
|
|
7
|
+
wuptracker config [show | set <k> <v> | unset <k> | get <k> | api-key <chave>]
|
|
8
|
+
wuptracker config helper # assistente interativo (primeira vez? comece aqui)
|
|
9
|
+
|
|
10
|
+
SESSÃO pode ser: o número mostrado no `show`, o nome da pasta, um caminho, ou
|
|
11
|
+
vazio (usa a mais recente). `--style` aceita vários: --style tecnico,linkedin
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from . import common
|
|
19
|
+
from . import config
|
|
20
|
+
from . import profiles
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fmt_dur(d):
|
|
24
|
+
return d or "em andamento"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_show(a):
|
|
28
|
+
sessions = common.list_sessions()
|
|
29
|
+
if not sessions:
|
|
30
|
+
print("Nenhuma sessão em " + config.SESSIONS_DIR + "/")
|
|
31
|
+
return
|
|
32
|
+
w = max(len(s["room"]) for s in sessions)
|
|
33
|
+
print(common.c_cyan(
|
|
34
|
+
f" # {'SESSÃO'.ljust(w)} DATA/HORA CAPS PERFIL STATUS"))
|
|
35
|
+
for i, s in enumerate(sessions, 1):
|
|
36
|
+
when = s["started_at"].replace("T", " ")[:16]
|
|
37
|
+
caps = str(s["captures"])
|
|
38
|
+
if s["pending"]:
|
|
39
|
+
caps += common.c_yellow(f"(+{s['pending']}?)")
|
|
40
|
+
status = []
|
|
41
|
+
if s["ongoing"]:
|
|
42
|
+
status.append(common.c_yellow("ativa"))
|
|
43
|
+
elif s.get("crashed"):
|
|
44
|
+
status.append(common.c_red("interrompida"))
|
|
45
|
+
if s["has_writeup"]:
|
|
46
|
+
status.append(common.c_green("writeup"))
|
|
47
|
+
line = (f" {i:>2} {s['room'].ljust(w)} {when} "
|
|
48
|
+
f"{caps:>4} {s['profile'].ljust(9)} {' '.join(status)}")
|
|
49
|
+
print(line)
|
|
50
|
+
print(common.c_dim(f"\n {len(sessions)} sessão(ões). "
|
|
51
|
+
f"wuptracker generate <#> para gerar o writeup."))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_styles(a):
|
|
55
|
+
print(common.c_cyan("Estilos de writeup (--style):"))
|
|
56
|
+
desc = {
|
|
57
|
+
"tecnico": "denso e preciso, seções fixas, horários (padrão)",
|
|
58
|
+
"corrido": "narrativa fluida em texto corrido",
|
|
59
|
+
"resumo": "resumo executivo curto (~250 palavras)",
|
|
60
|
+
"blog": "artigo técnico didático (dev.to / Medium)",
|
|
61
|
+
"linkedin": "post pronto para o LinkedIn (texto puro, hashtags)",
|
|
62
|
+
}
|
|
63
|
+
for k, v in desc.items():
|
|
64
|
+
mark = common.c_dim(" [com imagens]") if k in profiles.STYLES_WITH_IMAGES else ""
|
|
65
|
+
print(f" {common.c_green(k.ljust(9))} {v}{mark}")
|
|
66
|
+
print(common.c_dim("\n Perfis (--profile): thm | generico"))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _resolve_session(token):
|
|
70
|
+
sessions = common.list_sessions()
|
|
71
|
+
if token is None:
|
|
72
|
+
if not sessions:
|
|
73
|
+
sys.exit("[!] Nenhuma sessão. Rode 'wuptracker capture' primeiro.")
|
|
74
|
+
return sessions[0]["dir"]
|
|
75
|
+
if token.isdigit():
|
|
76
|
+
idx = int(token) - 1
|
|
77
|
+
if not (0 <= idx < len(sessions)):
|
|
78
|
+
sys.exit(f"[!] Sessão #{token} não existe. Veja 'wuptracker show'.")
|
|
79
|
+
return sessions[idx]["dir"]
|
|
80
|
+
if os.path.isdir(token):
|
|
81
|
+
return token
|
|
82
|
+
cand = os.path.join(config.SESSIONS_DIR, token)
|
|
83
|
+
if os.path.isdir(cand):
|
|
84
|
+
return cand
|
|
85
|
+
sys.exit(f"[!] Sessão não encontrada: {token}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_capture(a):
|
|
89
|
+
from . import capture as capture_mod
|
|
90
|
+
|
|
91
|
+
capture_mod.capture(a.name, a.profile)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_generate(a):
|
|
95
|
+
from . import generate as generate_mod
|
|
96
|
+
|
|
97
|
+
session_dir = _resolve_session(a.session)
|
|
98
|
+
generate_mod.generate(session_dir, a.style, a.profile, a.export, a.open)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_config(a):
|
|
102
|
+
from . import configtool
|
|
103
|
+
|
|
104
|
+
act, rest = a.action or "show", a.rest
|
|
105
|
+
if act == "show":
|
|
106
|
+
configtool.show()
|
|
107
|
+
elif act == "set":
|
|
108
|
+
if len(rest) < 2:
|
|
109
|
+
sys.exit("uso: wuptracker config set <chave> <valor>")
|
|
110
|
+
configtool.set_(rest[0], " ".join(rest[1:]))
|
|
111
|
+
elif act == "get":
|
|
112
|
+
if not rest:
|
|
113
|
+
sys.exit("uso: wuptracker config get <chave>")
|
|
114
|
+
k = configtool._resolve_key(rest[0])
|
|
115
|
+
print(configtool._fmt(getattr(config, k, None)))
|
|
116
|
+
elif act == "unset":
|
|
117
|
+
if not rest:
|
|
118
|
+
sys.exit("uso: wuptracker config unset <chave>")
|
|
119
|
+
configtool.unset_(rest[0])
|
|
120
|
+
elif act in ("api-key", "apikey"):
|
|
121
|
+
if not rest:
|
|
122
|
+
sys.exit("uso: wuptracker config api-key <chave>")
|
|
123
|
+
configtool.set_api_key(rest[0])
|
|
124
|
+
elif act == "backend":
|
|
125
|
+
if not rest:
|
|
126
|
+
sys.exit("uso: wuptracker config backend "
|
|
127
|
+
"<claude_cli|anthropic|ollama|openai|gemini>")
|
|
128
|
+
configtool.set_("backend", rest[0])
|
|
129
|
+
elif act in ("helper", "wizard", "setup"):
|
|
130
|
+
configtool.wizard()
|
|
131
|
+
else:
|
|
132
|
+
sys.exit(f"[!] ação de config desconhecida: {act}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_parser():
|
|
136
|
+
p = argparse.ArgumentParser(
|
|
137
|
+
prog="wuptracker", description=__doc__,
|
|
138
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
139
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
140
|
+
|
|
141
|
+
c = sub.add_parser("capture", help="inicia o modo de captura")
|
|
142
|
+
c.add_argument("name", nargs="?", help="nome da sessão")
|
|
143
|
+
c.add_argument("--profile", help="thm | generico (padrão: config.DEFAULT_PROFILE)")
|
|
144
|
+
c.set_defaults(func=cmd_capture)
|
|
145
|
+
|
|
146
|
+
s = sub.add_parser("show", help="lista as sessões")
|
|
147
|
+
s.add_argument("--writeups", action="store_true",
|
|
148
|
+
help="(reservado) só sessões já com writeup")
|
|
149
|
+
s.set_defaults(func=cmd_show)
|
|
150
|
+
|
|
151
|
+
g = sub.add_parser("generate", help="gera o writeup de uma sessão")
|
|
152
|
+
g.add_argument("session", nargs="?",
|
|
153
|
+
help="número do 'show', nome, caminho, ou vazio p/ a mais recente")
|
|
154
|
+
g.add_argument("--style", help="tecnico|corrido|resumo|blog|linkedin (vírgula p/ vários)")
|
|
155
|
+
g.add_argument("--profile", help="força thm | generico")
|
|
156
|
+
g.add_argument("--export", metavar="DIR", help="copia o(s) writeup(s) e assets para DIR")
|
|
157
|
+
g.add_argument("--open", action="store_true", help="abre o resultado no editor")
|
|
158
|
+
g.set_defaults(func=cmd_generate)
|
|
159
|
+
|
|
160
|
+
sub.add_parser("styles", help="lista estilos e perfis").set_defaults(func=cmd_styles)
|
|
161
|
+
|
|
162
|
+
cfg = sub.add_parser("config", help="ver/alterar configurações e integrações")
|
|
163
|
+
cfg.add_argument("action", nargs="?", default="show",
|
|
164
|
+
choices=["show", "set", "get", "unset", "api-key", "backend",
|
|
165
|
+
"helper"])
|
|
166
|
+
cfg.add_argument("rest", nargs="*", metavar="ARG")
|
|
167
|
+
cfg.set_defaults(func=cmd_config)
|
|
168
|
+
|
|
169
|
+
setup = sub.add_parser(
|
|
170
|
+
"setup", help="assistente interativo de configuração (atalho p/ config helper)")
|
|
171
|
+
setup.set_defaults(func=cmd_setup)
|
|
172
|
+
return p
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def cmd_setup(a):
|
|
176
|
+
from . import configtool
|
|
177
|
+
|
|
178
|
+
configtool.wizard()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main():
|
|
182
|
+
args = build_parser().parse_args()
|
|
183
|
+
args.func(args)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
main()
|