chore: auto-commit from dashboard

This commit is contained in:
2026-07-10 15:12:22 +00:00
parent 7b44749e5a
commit 738dc0181a
13 changed files with 383 additions and 44 deletions

View File

@@ -114,5 +114,10 @@
"memory": "memory/",
"logs": "logs/",
"sessions": "sessions/"
},
"roa2web": {
"base_url": "https://roa2web.romfast.ro/roa2web/api",
"default_company": "114",
"default_company_name": "ROMFAST SRL"
}
}

View File

@@ -11,9 +11,9 @@
"report_on": "changes",
"timeout": 120,
"enabled": true,
"last_run": "2026-07-09T10:00:00.001988+00:00",
"last_run": "2026-07-10T10:00:00.001575+00:00",
"last_status": "ok",
"next_run": "2026-07-09T16:00:00+00:00"
"next_run": "2026-07-10T16:00:00+00:00"
},
{
"name": "security-audit-daily",
@@ -43,9 +43,9 @@
"report_on": "never",
"timeout": 120,
"enabled": true,
"last_run": "2026-07-09T03:30:00.002154+00:00",
"last_run": "2026-07-10T03:30:00.002026+00:00",
"last_status": "ok",
"next_run": "2026-07-10T03:30:00+00:00"
"next_run": "2026-07-11T03:30:00+00:00"
},
{
"name": "archive-tasks-daily",
@@ -59,9 +59,9 @@
"report_on": "changes",
"timeout": 60,
"enabled": true,
"last_run": "2026-07-09T03:00:00.001431+00:00",
"last_run": "2026-07-10T03:00:00.002135+00:00",
"last_status": "ok",
"next_run": "2026-07-10T03:00:00+00:00"
"next_run": "2026-07-11T03:00:00+00:00"
},
{
"name": "backup-config",
@@ -75,9 +75,9 @@
"report_on": "never",
"timeout": 120,
"enabled": true,
"last_run": "2026-07-09T02:00:00.001391+00:00",
"last_run": "2026-07-10T02:00:00.002082+00:00",
"last_status": "ok",
"next_run": "2026-07-10T02:00:00+00:00"
"next_run": "2026-07-11T02:00:00+00:00"
},
{
"name": "insights-extract",
@@ -243,9 +243,9 @@
"prompt": "Heartbeat check. Rulează src/heartbeat.py printr-un scurt raport de status.\nDacă nu e nimic de raportat (email=0, calendar nu are evenimente <2h, kb ok), răspunde doar cu HEARTBEAT_OK și oprește-te — nu trimite mesaj.\nDacă e ceva: raport scurt pe Discord #echo-work.",
"allowed_tools": [],
"enabled": true,
"last_run": "2026-07-09T14:00:00.001126+00:00",
"last_run": "2026-07-10T14:00:00.002633+00:00",
"last_status": "ok",
"next_run": "2026-07-09T18:00:00+00:00"
"next_run": "2026-07-10T18:00:00+00:00"
},
{
"name": "night-execute",

View File

@@ -1,5 +1,5 @@
{
"last_sent": 25,
"last_sent": 26,
"year": 2026,
"last_sent_at": "2026-07-02T19:00:58.783724+00:00"
"last_sent_at": "2026-07-09T15:01:09.525808+00:00"
}

View File

@@ -1,5 +1,21 @@
{
"notes": [
{
"file": "notes-data/youtube/2026-07-10_opencode-token-usage-96-percent.md",
"title": "I Cut My OpenCode Token Usage by 96% - Here's How",
"date": "2026-07-10",
"tags": [],
"domains": [
"work",
"growth"
],
"types": [],
"category": "youtube",
"project": null,
"subdir": null,
"video": "",
"tldr": "Autorul folosește un proxy man-in-the-middle pentru a vedea exact ce trimite OpenCode (agent de coding AI) către LLM la un simplu \"hello\" — și găsește 8.000 de tokeni: system prompt de 9.500 caractere..."
},
{
"file": "notes-data/youtube/2026-07-08_fine-tune-modele-mari-kimi-k27.md",
"title": "Fine-Tune the biggest open-source models (even with a bad PC)",
@@ -10192,11 +10208,11 @@
}
],
"stats": {
"total": 589,
"total": 590,
"by_domain": {
"work": 201,
"work": 202,
"health": 104,
"growth": 274,
"growth": 275,
"sprijin": 39,
"scout": 9
},
@@ -10213,7 +10229,7 @@
"reflectii": 3,
"retete": 1,
"tools": 7,
"youtube": 158,
"youtube": 159,
"memory": 44
}
},

View File

@@ -14,7 +14,7 @@
- **[reflectii/](reflectii/index.md)** — 3 note
- **[retete/](retete/index.md)** — 1 note
- **[tools/](tools/index.md)** — 7 note
- **[youtube/](youtube/index.md)** — 158 note
- **[youtube/](youtube/index.md)** — 159 note
## Note la rădăcină

View File

@@ -0,0 +1,48 @@
# I Cut My OpenCode Token Usage by 96% - Here's How
**URL:** https://youtu.be/FX7jcd3GYtI
**Durata:** 9:42
**Data salvat:** 2026-07-10
**Tags:** @work @growth @ai @agents @llm
---
## TL;DR
Autorul folosește un proxy man-in-the-middle pentru a vedea exact ce trimite OpenCode (agent de coding AI) către LLM la un simplu "hello" — și găsește 8.000 de tokeni: system prompt de 9.500 caractere + 11 definiții de tool-uri (bash, task, edit, read etc.) trimise la FIECARE request, plus un al doilea apel LLM ascuns doar pentru titlul conversației. Soluția: creează un agent custom minimal (fără tools, system prompt scurt) → 300-500 tokeni, reducere ~90-96%. Recomandare: pornește minimal, adaugă tools doar când chiar ai nevoie de ele.
---
## Puncte cheie
- **8.000 tokeni pentru "hello"** — 0,025% e mesajul propriu-zis, restul e scaffolding (system prompt + tool definitions)
- OpenCode face **2 apeluri LLM** per mesaj: unul ascuns doar ca să genereze titlul conversației (~2.000 tokeni, pt un titlu de 1 cuvânt)
- System prompt default: ~114 linii, spune LLM-ului tot ce poate/nu poate face, format, emoji, etc.
- 11 tool definitions trimise mereu — bash tool singur = 4.700 caractere, task tool = 3.000 caractere
- **Cache-ul providerului NU te ajută pe tine** — reduce costul $ pentru provider, dar tot trimiți full payload pe rețea, tot consumi context window și latență
- Soluție: agenți custom minimali (folder `.opencode/agents/*.md`) cu system prompt scurt și zero/puține tools, setați ca `primary` — switch cu Tab între agent complet (build) și agent lean
- Trade-off: agentul lean nu are task/sub-agents, web fetch, etc. — dar poți adăuga selectiv doar ce ai nevoie
- Filosofie: **start minimal, expand when necessary** — nu monta toate tool-urile/MCP-urile by default
---
## Quote-uri relevante
> "8,000 tokens. That's what it cost me to say hello to an AI coding agent."
> "Cached tokens are a cost optimization for the provider, not you. For you, it's still 8,000 tokens of latency and context window usage."
> "Start minimal and expand when necessary."
---
## Idei acționabile pentru Marius
- [ ] Relevant pentru Claude Code / echo-core: verifică dacă subagenții custom (`Agent` tool) pot fi definiți cu scope minim (fără tool-uri inutile) pentru task-uri simple, ca să reduci tokeni pe apelurile dese (heartbeat, routine checks) @work
- [ ] Ideea de "agent lean vs agent complet, switch la nevoie" se mapează pe strategia ta Haiku/Sonnet din AGENTS.md — poate merită un al treilea nivel: agent minimal fără tools pentru întrebări simple @growth
---
## Surse menționate
- Repo GitHub cu scriptul proxy + config agent (menționat, link nu apare explicit în transcript)

View File

@@ -1,6 +1,6 @@
# Index — youtube/
> 158 note. Citește acest index întâi; deschide doar fișierele relevante.
> 159 note. Citește acest index întâi; deschide doar fișierele relevante.
- **[Talk to Claude on 3CX Phone System Tutorial (Full Setup)](2025-02-13_talk-to-claude-3cx-phone.md)** `@work`
Tutorial complet pentru a vorbi cu Claude Code prin telefon, folosind 3CX (sistem telefonic cloud gratuit) + un proiect GitHub custom. Setup
@@ -316,5 +316,7 @@
Chris Koerner explică cum pornești o afacere cu $1k-$5k și ce principii l-au dus la succes cu 35+ proprietăți RV și multiple business-uri. E
- **[Fine-Tune the biggest open-source models (even with a bad PC)](2026-07-08_fine-tune-modele-mari-kimi-k27.md)** `@work @growth`
Tutorial complet despre cum se fine-tunează modele AI masive (ex: Kimi K2.7 — 1 trilion parametri) pe date proprii, fără hardware de sute de
- **[I Cut My OpenCode Token Usage by 96% - Here's How](2026-07-10_opencode-token-usage-96-percent.md)** `@work @growth`
Autorul folosește un proxy man-in-the-middle pentru a vedea exact ce trimite OpenCode (agent de coding AI) către LLM la un simplu "hello" —
- **[thinking-on-paper](thinking-on-paper.md)** `@growth`
Metoda "Thinking on Paper" — 3 principii: **Make it Wrong** (scrie repede, fără perfecționism), **Make it Shorter** (doar keywords), **Make

View File

@@ -787,6 +787,11 @@ def cmd_audio(args: list[str]) -> str:
if len(text) > _MAX_TTS_CHARS:
text = text[:_MAX_TTS_CHARS] + "..."
# Normalizare RO pentru TTS (numere, monedă, unități, abrevieri) —
# aceeași expandare ca voice mode live, fără trunchierea la 200 cuvinte
from src.voice.normalize import expand_for_tts
text = expand_for_tts(text)
# Apel TTS (import direct din tools/tts.py)
result = _tts_synthesize(text, voice)
if result.get("ok"):

View File

@@ -201,8 +201,12 @@ _CURRENCY_SUB = {
}
_CURRENCY_PATTERNS = [
# RON suffix (case-insensitive: RON, ron, lei)
(re.compile(r'(?<!\w)(\d+(?:\.\d+)?)\s+(?:RON|lei)\b', re.IGNORECASE), 'RON'),
# RON suffix (case-insensitive: RON, ron, lei). Accepts either a
# dot-decimal fraction ("12.50 lei") or, since Romanian accounting
# format uses comma for decimals ("53.600,00 lei" — the dot-thousands
# group is already collapsed by normalize_thousands upstream), a
# comma-decimal fraction in its own capture group.
(re.compile(r'(?<!\w)(\d+(?:\.\d+)?)(?:,(\d{1,2}))?\s+(?:RON|lei)\b', re.IGNORECASE), 'RON'),
# Prefix currencies
(re.compile(r'\$(\d+(?:\.\d+)?)'), 'USD'),
(re.compile(r'€(\d+(?:\.\d+)?)'), 'EUR'),
@@ -222,25 +226,28 @@ def _format_currency_unit(n: int, singular: str, plural: str) -> str:
return f"{word} {plural}"
def _format_currency(amount: str, code: str) -> str:
def _format_currency(amount: str, code: str, comma_frac: str | None = None) -> str:
main_sg, main_pl = _CURRENCY_MAIN[code]
if '.' in amount:
if comma_frac is not None:
whole_s, frac_s = amount, comma_frac
elif '.' in amount:
whole_s, frac_s = amount.split('.', 1)
# Normalize fractional part to 2 digits so "12.5 RON" reads as
# 50 bani, not 5 bani.
if len(frac_s) == 1:
frac_s = frac_s + '0'
elif len(frac_s) > 2:
frac_s = frac_s[:2]
whole = int(whole_s)
frac = int(frac_s)
whole_part = _format_currency_unit(whole, main_sg, main_pl)
if frac == 0:
return whole_part
sub_sg, sub_pl = _CURRENCY_SUB[code]
frac_part = _format_currency_unit(frac, sub_sg, sub_pl)
return f"{whole_part} și {frac_part}"
return _format_currency_unit(int(amount), main_sg, main_pl)
else:
return _format_currency_unit(int(amount), main_sg, main_pl)
# Normalize fractional part to 2 digits so "12.5 RON" reads as
# 50 bani, not 5 bani.
if len(frac_s) == 1:
frac_s = frac_s + '0'
elif len(frac_s) > 2:
frac_s = frac_s[:2]
whole = int(whole_s)
frac = int(frac_s)
whole_part = _format_currency_unit(whole, main_sg, main_pl)
if frac == 0:
return whole_part
sub_sg, sub_pl = _CURRENCY_SUB[code]
frac_part = _format_currency_unit(frac, sub_sg, sub_pl)
return f"{whole_part} și {frac_part}"
def expand_currency(text: str) -> str:
@@ -251,7 +258,10 @@ def expand_currency(text: str) -> str:
bani / cenți / pence).
"""
for pattern, code in _CURRENCY_PATTERNS:
text = pattern.sub(lambda m, c=code: _format_currency(m.group(1), c), text)
def _sub(m, c=code):
comma_frac = m.group(2) if m.re.groups >= 2 else None
return _format_currency(m.group(1), c, comma_frac)
text = pattern.sub(_sub, text)
return text
@@ -296,12 +306,13 @@ _MAX_WORDS = 200
_TRUNCATE_SUFFIX = "Restul l-am scris în chat."
def normalize_for_tts(text: str) -> str:
"""Apply the full normalization pipeline and truncate to 200 words.
def expand_for_tts(text: str) -> str:
"""Apply the full normalization pipeline (markdown strip, abbreviations,
numbers, currency, units, symbols) WITHOUT the 200-word truncation.
If the text exceeds 200 words, the first 200 are kept and the suffix
"Restul l-am scris în chat." is appended so the listener knows the
response continues in the text channel mirror.
Use this for one-shot TTS generation (e.g. /audio command) where the
"Restul l-am scris în chat." suffix from normalize_for_tts() would be
misleading (no live chat mirror exists for that flow).
"""
text = strip_markdown(text)
text = sanitize_punctuation(text)
@@ -312,6 +323,17 @@ def normalize_for_tts(text: str) -> str:
text = expand_units(text)
text = expand_numbers_ro(text)
text = expand_symbols(text)
return text.strip()
def normalize_for_tts(text: str) -> str:
"""Apply the full normalization pipeline and truncate to 200 words.
If the text exceeds 200 words, the first 200 are kept and the suffix
"Restul l-am scris în chat." is appended so the listener knows the
response continues in the text channel mirror.
"""
text = expand_for_tts(text)
words = text.split()
if len(words) > _MAX_WORDS:
text = ' '.join(words[:_MAX_WORDS]) + f" {_TRUNCATE_SUFFIX}"

View File

@@ -78,6 +78,28 @@ def test_expand_currency(text, expected):
assert expand_currency(text) == expected
# ============================================================
# Currency — Romanian accounting format (comma decimal, dot
# thousands already collapsed upstream by normalize_thousands)
# ============================================================
@pytest.mark.parametrize("text,expected", [
("53600,00 lei", "cincizeci și trei de mii șase sute de lei"),
("17600,50 lei", "șaptesprezece mii șase sute de lei și cincizeci de bani"),
("100,00 lei", "o sută de lei"),
("17600,05 lei", "șaptesprezece mii șase sute de lei și cinci bani"),
])
def test_expand_currency_comma_decimal(text, expected):
assert expand_currency(text) == expected
@pytest.mark.parametrize("text,expected", [
("53.600,00 lei", "cincizeci și trei de mii șase sute de lei"),
("17.600,50 lei", "șaptesprezece mii șase sute de lei și cincizeci de bani"),
])
def test_normalize_for_tts_currency_ro_accounting_format(text, expected):
assert normalize_for_tts(text) == expected
# ============================================================
# Symbols (4)
# ============================================================

View File

@@ -6,7 +6,7 @@
"D406": "ca6103448d663ab16fcaef0f29f8933ef526cbf5aad12c7ff5dbd61b22ca9fc6",
"SIT_FIN_SEM_2025": "8164843431e6b703a38fbdedc7898ec6ae83559fe10f88663ba0b55f3091d5fe",
"SIT_FIN_AN_2025": "accceef5b6585a3e901d83d23fc2e60f6562eac4a2ce00f943856232bed929d6",
"DESCARCARE_DECLARATII": "0fb1535fe97aeadc7f415431238fae2cf7bc52d9e0c17ce3c4b4a12db92cd733",
"DESCARCARE_DECLARATII": "1abf61068fc38e76963520f455a87635efdcf6c126fbf913d8d860478d3fc5ba",
"D205": "d3c20a7ae70f4c18bbb7add42af035e3746d323b2e6df37a4e31ed625ddb86d9",
"D390": "31d0bf3f98eda3556039cc7e186bfcaf71bdb962459a2cf54f4d03d1d57053fc",
"BILANT_2024": "fbb8d66c2e530d8798362992c6983e07e1250188228c758cb6da4cde4f955950",

View File

@@ -507,6 +507,11 @@ L153
Se transmit prin
portalul
e-guvernare.ro
Formularul
S007
Se transmit prin
portalul
e-guvernare.ro
C801
- Cerere de atribuire a numărului unic de identificare din aplicaţia
informatică a ANAF - publicat în data de

214
tools/roa2web_client.py Normal file
View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
Client generic pentru API-ul roa2web — autentificare + query-uri rapoarte.
Autentificare: user/parolă Oracle (ca la login-ul normal ROA) + 2FA email la
prima logare. După primul login cu trust_device=True, sesiunile următoare
folosesc refresh_token din keyring — fără OTP.
Credențiale (keyring, service "echo-core", vezi src/credential_store.py):
roa2web_user, roa2web_pass, roa2web_server,
roa2web_access_token, roa2web_refresh_token, roa2web_trusted_device_token
Firma implicită și base_url sunt în config.json -> "roa2web".
Usage:
python3 tools/roa2web_client.py summary [--company COD_SAU_NUME] [--luna N] [--an N]
python3 tools/roa2web_client.py companies
python3 tools/roa2web_client.py get <path> [--company COD_SAU_NUME] [--param k=v ...]
python3 tools/roa2web_client.py verify-2fa <cod> <email>
"""
import argparse
import json
import sys
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
import requests
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src.credential_store import get_secret, set_secret
from src.config import Config
class ROA2WebError(Exception):
pass
class ROA2WebClient:
def __init__(self):
self.config = Config()
self.base_url = self.config.get("roa2web.base_url", "https://roa2web.romfast.ro/roa2web/api")
self.session = requests.Session()
self._token = get_secret("roa2web_access_token")
parsed = urlsplit(self.base_url)
self._origin = f"{parsed.scheme}://{parsed.netloc}"
# IIS/ARR reverse-proxy-ul din fata backend-ului serveste totul sub /roa2web,
# dar FastAPI nu stie de acest prefix -> redirect-urile lui (307, ex. trailing
# slash pe rutele definite la "/") vin fara el si pica in 404. Il reinjectam.
self._prefix = parsed.path[: -len("/api")] if parsed.path.endswith("/api") else parsed.path
@property
def default_company(self) -> str:
return self.config.get("roa2web.default_company", "114")
def _store_tokens(self, data: dict) -> None:
set_secret("roa2web_access_token", data["access_token"])
if data.get("refresh_token"):
set_secret("roa2web_refresh_token", data["refresh_token"])
if data.get("trusted_device_token"):
set_secret("roa2web_trusted_device_token", data["trusted_device_token"])
self._token = data["access_token"]
def _login_password(self) -> None:
user = get_secret("roa2web_user")
pw = get_secret("roa2web_pass")
server = get_secret("roa2web_server") or "romfast"
if not user or not pw:
raise ROA2WebError("Lipsesc roa2web_user/roa2web_pass din keyring (eco secrets set ...)")
payload = {"username": user, "password": pw, "server_id": server}
trusted = get_secret("roa2web_trusted_device_token")
if trusted:
payload["trusted_device_token"] = trusted
r = self.session.post(f"{self.base_url}/auth/login", json=payload, timeout=15)
r.raise_for_status()
data = r.json()
if data.get("requires_2fa"):
raise ROA2WebError(
f"Necesar cod OTP nou, trimis pe {data.get('masked_email')}. "
"Cere-i lui Marius codul, apoi ruleaza verify_2fa(code, email)."
)
self._store_tokens(data)
def _refresh(self) -> bool:
refresh_token = get_secret("roa2web_refresh_token")
if not refresh_token:
return False
r = self.session.post(
f"{self.base_url}/auth/refresh",
json={"refresh_token": refresh_token},
timeout=15,
)
if r.status_code != 200:
return False
self._store_tokens(r.json())
return True
def verify_2fa(self, code: str, email: str, server_id: str | None = None) -> dict:
server = server_id or get_secret("roa2web_server") or "romfast"
r = self.session.post(
f"{self.base_url}/auth/verify-2fa-code",
json={"code": code, "email": email, "server_id": server, "trust_device": True},
timeout=15,
)
r.raise_for_status()
data = r.json()
self._store_tokens(data)
return data
def request(self, method: str, path: str, params: dict | None = None, _retried: bool = False) -> dict:
if not self._token and not self._refresh():
self._login_password()
url = f"{self.base_url}{path}" if path.startswith("/") else f"{self.base_url}/{path}"
headers = {"Authorization": f"Bearer {self._token}"}
r = self.session.request(method, url, params=params, headers=headers, timeout=30, allow_redirects=False)
if r.status_code in (307, 308) and "Location" in r.headers:
loc = urlsplit(r.headers["Location"])
loc_path = loc.path
if self._prefix and not loc_path.startswith(self._prefix):
loc_path = self._prefix + loc_path
fixed_url = urlunsplit((urlsplit(self.base_url).scheme, urlsplit(self.base_url).netloc, loc_path, loc.query, ""))
r = self.session.request(method, fixed_url, headers=headers, timeout=30)
if r.status_code == 401 and not _retried:
if self._refresh() or (self._login_password() or True):
return self.request(method, path, params=params, _retried=True)
r.raise_for_status()
return r.json()
def get(self, path: str, params: dict | None = None) -> dict:
return self.request("GET", path, params=params)
def companies(self) -> list:
return self.get("/companies").get("companies", [])
def dashboard_summary(self, company: str | None = None, luna: int | None = None, an: int | None = None) -> dict:
params = {"company": company or self.default_company}
if luna:
params["luna"] = luna
if an:
params["an"] = an
return self.get("/reports/dashboard/summary", params)
def resolve_company(client: ROA2WebClient, value: str | None) -> str:
"""Acceptă cod numeric sau (parte din) numele firmei; implicit firma din config."""
if not value:
return client.default_company
if value.isdigit():
return value
needle = value.lower()
matches = [c for c in client.companies() if needle in c["name"].lower()]
if not matches:
raise ROA2WebError(f"Firma '{value}' nu a fost gasita in lista de firme.")
if len(matches) > 1:
names = ", ".join(f"{c['id_firma']}={c['name']}" for c in matches)
raise ROA2WebError(f"'{value}' e ambiguu, se potriveste cu mai multe firme: {names}")
return str(matches[0]["id_firma"])
def main():
parser = argparse.ArgumentParser(description="Client CLI pentru roa2web API")
sub = parser.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("summary", help="Dashboard financiar (trezorerie, clienti, furnizori)")
p.add_argument("--company", help="Cod sau nume firma (implicit: cea din config.json)")
p.add_argument("--luna", type=int)
p.add_argument("--an", type=int)
sub.add_parser("companies", help="Lista firmelor disponibile")
p = sub.add_parser("get", help="Query generic pe orice endpoint /api/...")
p.add_argument("path", help='ex: "/reports/invoices" sau "/reports/treasury/bank-cash-register"')
p.add_argument("--company", help="Daca e dat, se adauga ca param 'company'")
p.add_argument("--param", action="append", default=[], metavar="k=v", help="Parametru query, repetabil")
v = sub.add_parser("verify-2fa", help="Finalizeaza login-ul cu un cod OTP primit pe email")
v.add_argument("code")
v.add_argument("email")
args = parser.parse_args()
client = ROA2WebClient()
try:
if args.cmd == "summary":
company = resolve_company(client, args.company)
data = client.dashboard_summary(company=company, luna=args.luna, an=args.an)
elif args.cmd == "companies":
for c in client.companies():
print(f"{c['id_firma']}\t{c['name']}")
return
elif args.cmd == "get":
params = dict(kv.split("=", 1) for kv in args.param)
if args.company:
params["company"] = resolve_company(client, args.company)
data = client.get(args.path, params or None)
elif args.cmd == "verify-2fa":
raw = client.verify_2fa(args.code, args.email)
data = {k: v for k, v in raw.items() if k not in ("access_token", "refresh_token", "trusted_device_token")}
except ROA2WebError as e:
print(f"Eroare: {e}", file=sys.stderr)
sys.exit(1)
print(json.dumps(data, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()