Files
Claude Agent ed7eca4fc7 fix(discord): watchdog de gateway — procesul viu pe un gateway mort nu mai trece neobservat
Puntea a tacut ~7 ore fara ca nimic sa semnaleze: discord.py memoreaza
`resume_gateway_url` primit la ultimul READY si il refoloseste la fiecare
reconectare. Cand gatewayul regional (gateway-us-east-1a) a inceput sa dea
503 la handshake, botul a reincercat la infinit acelasi host mort, cu backoff
pana la ~15 minute. Procesul era viu, deci nici systemd nici dashboardul nu
aveau ce vedea, iar `gateway.discord.gg` raspundea normal tot timpul.

GatewayWatchdog numara de cat timp e legatura jos (on_connect / on_resumed /
on_ready o ridica, on_disconnect o coboara, iar reincercarile esuate nu
reseteaza ceasul). Peste `GATEWAY_WATCHDOG_S` (implicit 300s) alerteaza, iese
cu codul 3 si lasa systemd sa reporneasca — restartul e singurul lucru care
forteaza un IDENTIFY nou pe gateway.discord.gg. Un prag <= 0 il dezactiveaza.

O iesire la 300s ramane sub StartLimitBurst=5/5min, deci bucla de restart nu
poate ajunge sa lase unitul `failed`. `_hard_exit_after` acopera cazul in care
`close()` se blocheaza pe socketul mort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-09-01 07:37:38 +00:00

22 KiB

Punte Discord -> Claude Code (LXC 171)

Care chatbot e care? Exista trei lucruri numite „Maria", doi boti pe Discord si doua punti WhatsApp legate la acelasi numar. Inainte de a depana, citeste docs/chatboti-si-punti.md — spune care instanta raspunde de fapt.

Un bot Discord subtire care duce mesajele dintr-un guild privat catre CLI-ul claude care ruleaza pe containerul de dezvoltare LXC 171 (claude-agent, 10.0.20.171), si aduce raspunsurile inapoi. Practic: acelasi Claude Code cu care lucrezi in terminal, comandat de pe telefon.

Nu e un chatbot separat. Nu are memorie proprie, nu are baza de date proprie: sesiunile sunt chiar sesiunile Claude Code din ~/.claude/projects/, iar un fir de Discord este o sesiune.

Nu confunda cu MoltBot (LXC 110) sau cu OpenClaw — acelea sunt alti agenti, cu alt scop. Puntea asta ruleaza pe masina de dezvoltare si are accesul ei.


Arhitectura

Discord (guild privat)
   | on_message
   v
bot.py -- allowlist (guild / canal / utilizator)    [doar adaptor Discord]
   |
   +-- session_store.py   state.json {thread_id: {sid, cwd, model, inflight, pid}}
   +-- runner.py          proces persistent per fir, alimentat pe stdin
   +-- stream.py          parser tolerant de JSONL
   +-- render.py          chunker + un loop de editare per canal
   +-- limits.py          max procese, timeout tur, rate limit, plafon de cost
   +-- security/          hook PreToolUse (confirmari) + wrapper `infra`
   +-- alerts.py          alerte email                          [ops]
   +-- cleanup.py         procese lasate in urma (`/cleanup`)   [ops]
   |
   v
claude -p --input-format stream-json --output-format stream-json --verbose
       --resume <sid> --permission-mode bypassPermissions
       --settings ~/.claude-discord/bot-settings.json --model sonnet --autocompact auto

Cateva alegeri care nu se vad din diagrama:

  • Proces persistent per fir, nu unul per mesaj. Asta permite steering la mijlocul turului: un mesaj trimis in timp ce Claude lucreaza ajunge la el si schimba raspunsul (verificat: mesaj la 8s intr-un tur de 34.5s). Reaper la 20 min de inactivitate; repornirea se face cu --resume <sid>, deci firul nu-si pierde contextul.
  • Un proces claude = ~406 MB RSS (masurat). De aici toate limitele: maxim 4 procese vii, MemoryMax=6G pe unit, si comanda /cleanup.
  • Model implicit sonnet. Un tur banal pe opus a costat $0.1547 (masurat), deci opus e optional, per fir, prin /model model:opus.

Comenzi

Comanda Ce face
/new Sesiune noua, curata, in firul curent
/new fork:True Sesiune noua care porneste din contextul celei curente
/cd cale:<cale> Schimba directorul de lucru al firului (ex. /cd cale:/workspace/romfastsql)
/model model:<sonnet|opus> Schimba modelul pentru firul curent
/status Sesiune, director, model, fereastra de utilizare, cost cumulat, proces viu, ultimele linii de stderr
/stop Opreste turul in desfasurare din firul curent
/cleanup Listeaza procesele lasate in urma (rulare seaca). /cleanup force:True le opreste
/permisiuni Ce s-a aprobat pentru tot firul. /permisiuni revoca:True sterge aprobarile
/help Lista de mai sus, in fir

Comenzile sunt application commands (/), inregistrate pe guild-urile din DISCORD_GUILD_IDS la pornirea botului, deci apar in lista de comenzi a Discord. Vechiul prefix ! nu mai executa nimic: botul raspunde doar cu indiciul catre comanda / echivalenta. Orice alt mesaj din canal pleaca la Claude ca prompt, ca inainte.

Un fir de Discord = o sesiune Claude. Canalul principal are si el sesiunea lui, cea implicita. Subsolul fiecarui raspuns arata modelul, durata si costul.

Atasamente

Pozele si fisierele text trimise pe Discord ajung la Claude in acelasi tur cu mesajul. Un mesaj doar cu atasament, fara text, e valid — nu mai e respins ca gol. Merge si mid-tur: o poza trimisa in timpul unui tur intra pe stdin ca steering, nu deschide tur nou.

Tip Ce se intampla Limite
Imagini png, jpeg, gif, webp Devin blocuri image — Claude le vede max 4/mesaj, max 3,5 MB fiecare
Fisiere text (.txt, .md, .log, .csv, .json, .sql, .py, .sh, …) Continutul e inserat in prompt max 4/mesaj, max 100 KB fiecare (peste atat, trunchiat)
Orice altceva (PDF, Office, arhive, svg, heic) Doar numit in prompt, cu motivul —

Nimic nu dispare tacut: ce n-a putut fi citit (prea mare, tip neacceptat, descarcare esuata) apare la finalul promptului intr-o linie „Atasamente ignorate: …", deci Claude stie ca ai trimis ceva si poate cere altceva. O imagine stricata nu anuleaza restul mesajului.

Limita de 3,5 MB e pe octetii bruti fiindca base64 umfla cu ~4/3, iar API-ul refuza imaginile peste ~5 MB codate. Marimea e verificata de doua ori: intai cea declarata de Discord (ca sa nu descarcam degeaba), apoi cea reala, dupa descarcare.

Despre /cleanup

KillMode=control-group opreste arborele serviciului la restart, dar nu prinde ce s-a desprins: un server pornit cu & intr-un tur, un nohup, un job lung reparentat la init. Alea raman si se aduna — ~440 MB bucata cu tot cu serverele MCP, pe un container cu istoric de OOM.

DOMENIUL e cgroup-ul serviciului claude-discord.service si numai el. Pe LXC 171 ruleaza permanent sesiuni Claude interactive (tmux, ttyd, agenti) care n-au nicio legatura cu puntea; acelea stau in tmux-spawn-*.scope si NU sunt raportate niciodata, nici macar in rularea seaca. Regula e fail-closed: daca cgroup-ul unui proces nu poate fi citit, procesul e considerat neeligibil — mai bine ratam un orfan decat sa oprim sesiunea cuiva. Fara aceasta limitare, /cleanup force:True dat din Discord si-ar opri propria sesiune impreuna cu tot ce ruleaza omul in tmux.

Inauntrul cgroup-ului, e orfan orice proces neprotejat care nu se leaga de state.json, indiferent cum se numeste — asa raman prinse si serverele MCP pornite de claude (npm exec @playwright/mcp, node ...), care altfel ar scapa fiindca nu se cheama claude. Un fir costa in jur de 440 MB cu tot cu MCP, nu 300.

Ruleaza sec (dry-run) in mod implicit — intai vezi lista, apoi decizi. Ce e inregistrat in state.json si toti descendentii acelor procese (adica turul care ruleaza chiar acum) nu sunt niciodata atinse, iar potrivirea se face si pe pid_start_time, ca un PID reciclat sa nu duca la oprirea altui proces. Copiii se opresc inaintea parintilor, altfel s-ar reparenta si ar scapa.


Instalare

Partea automata

cd /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge
./ops/install.sh

Scriptul e idempotent (poti sa-l rulezi de cate ori vrei) si face:

  1. ~/.claude-discord/ cu drepturi 0700, plus logs/ si approvals/
  2. ~/.claude-discord/env cu 0600, copiat din ops/env.example — nu suprascrie niciodata un env existent
  3. venv in ~/.claude-discord/venv + dependintele din requirements.txt
  4. loginctl enable-linger claude — fara asta serviciul de utilizator moare la logout si nu porneste la boot
  5. symlink ~/.config/systemd/user/claude-discord.service -> ops/claude-discord.service, apoi daemon-reload si systemd-analyze verify
  6. intrare de crontab pentru logrotate (zilnic, 04:10)

Scriptul nu porneste serviciul. Dupa ce completezi env-ul:

./ops/install.sh --start

Partea manuala (o faci tu, o singura data)

Nu se poate automatiza: cere un om logat in Discord.

  1. Creeaza aplicatia Discord. https://discord.com/developers/applications -> New Application. E o aplicatie noua, dedicata puntii — nu refolosi aplicatia MoltBot/OpenClaw.

  2. Deschide sectiunea Bot. Portalul creeaza user-ul bot odata cu aplicatia, deci daca nu vezi un buton Add Bot e normal — intri direct in Bot.

  3. Ia token-ul. Bot -> Reset Token -> copiaza. Se arata o singura data. Il pui in ~/.claude-discord/env, la DISCORD_TOKEN=. Fisierul e 0600 si nu e versionat. Daca token-ul ajunge vreodata intr-un commit, reseteaza-l imediat din portal — cine il are poate comanda infrastructura.

  4. Activeaza intents. Bot -> Privileged Gateway Intents -> porneste MESSAGE CONTENT INTENT. Fara el botul primeste mesajele goale si nu face nimic. Dupa comutator apare jos bara Save Changes — daca nu o apesi, setarea NU se salveaza (usor de ratat pe telefon). Reincarca pagina si confirma ca a ramas pornit. Nu ai nevoie de aprobare de la Discord: verificarea e ceruta abia de la 100 de servere. (Server Members si Presence nu sunt necesare — lasa-le oprite.)

  5. Invita botul intr-un guild PRIVAT al tau. OAuth2 -> URL Generator -> scopes: bot si applications.commands -> permisiuni: View Channel, Send Messages, Read Message History, Create Public Threads, Send Messages in Threads, Attach Files, Embed Links, Add Reactions. Deschide URL-ul generat si alege serverul. Bifele sunt greu de nimerit pe telefon; linkul echivalent, gata calculat (APPLICATION_ID e in General Information):

    https://discord.com/oauth2/authorize?client_id=APPLICATION_ID&scope=bot%20applications.commands&permissions=309237763136
    

    309237763136 = exact permisiunile de mai sus. Fara View Channel botul nu vede canalul deloc, oricat de permis ar fi in allowlist. Fara scope-ul applications.commands botul merge, dar inregistrarea comenzilor / esueaza cu 403 Missing Access (scrie in log linkul de reinvitare) si comenzile nu apar in lista. Un bot deja invitat se re-invita cu acelasi link: se adauga doar scope-ul lipsa. Nu-l invita intr-un server cu alti oameni — cine scrie in canalul permis comanda direct containerul.

  6. Ia ID-urile pentru allowlist. In Discord: Settings -> Advanced -> Developer Mode pornit. Apoi click dreapta (pe telefon: apasare lunga) -> Copy Server ID / Copy Channel ID / Copy User ID. Le pui in ~/.claude-discord/env: DISCORD_GUILD_IDS, DISCORD_CHANNEL_IDS, DISCORD_USER_IDS (separate prin virgula). Allowlist gol = nimic permis (fail-closed). Mesajele de la webhook-uri si de la alti boti sunt ignorate din principiu.

  7. Pune destinatarul alertelor: ALERT_RECIPIENT= in acelasi env.

  8. Verifica plafonul de cost: COST_CAP_USD_DAY= (implicit off). Pe abonament (Pro/Max) lasa off: costul raportat de CLI e doar pretul echivalent la API, nu o factura. Pune un numar (ex. 5.00) doar daca platesti per token.

  9. Abia acum: ./ops/install.sh --start.


Operare

Unde te uiti

systemctl --user status claude-discord          # e viu?
journalctl --user -u claude-discord -n 200      # ce a facut ultima data
tail -f ~/.claude-discord/logs/bot.log          # logul aplicatiei
tail -f ~/.claude-discord/logs/alerts.log       # ce alerte s-au trimis / au esuat
systemctl --user restart claude-discord         # repornire

Aceleasi lucruri, cu butoane, in dashboard-ul de control (dashboard/README.md) — stare, restart, orfani, confirmari, jurnale:

https://claude-agent.tailf7372d.ts.net/claude    # din tailnet (ca /echo la moltbot)
                                                 # fara login: DASHBOARD_AUTH=off

Procesul e legat de 127.0.0.1:18790; in tailnet il publica tailscale serve. Fara Tailscale: ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171.

Acelasi panou include si sectiunea "Maria — WhatsApp + RAG", care controleaza proiectul sibling ../maria-whatsapp-bridge/ (punte WhatsApp Baileys + consumer RAG): start/stop/restart, cod QR de asociere, depozit de documente, reindexare si sincronizare Google Drive. Decizie deliberata: un singur dashboard comun pentru ambele punti de pe acest container, nu un panou separat per serviciu.

Fisier Ce e
~/.claude-discord/env token + allowlist + limite (0600)
~/.claude-discord/state.json sesiuni, directoare, pid-uri, cost
~/.claude-discord/logs/bot.log stdout/stderr al botului (rotit zilnic, 14 zile)
~/.claude-discord/logs/alerts.log jurnalul alertelor
~/.claude-discord/logs/dashboard.log stdout/stderr al dashboard-ului de control
~/.claude-discord/alerts-dedup.json fereastra de dedup a alertelor
~/.claude-discord/approvals/ cereri de confirmare intre hook si bot

Cost

Costul se vede in trei locuri: in subsolul fiecarui raspuns (turul curent + cumulat pe fir), in /status, si in state.json la cheia cost. E pretul echivalent la API raportat de CLI: pe abonament nu se factureaza, deci ramane doar o masura de consum.

Plafonul zilnic e optional. COST_CAP_USD_DAY=off (implicit, si 0 sau gol) inseamna fara plafon: cheltuiala se contorizeaza mai departe, dar nimic nu se opreste. Cu un numar pozitiv, la atingerea lui botul nu mai accepta tururi noi si trimite email; plafonul se reseteaza la schimbarea zilei.

Alerte pe email

alerts.py urmeaza tiparul deja folosit in repo (vezi proxmox/vm109-windows-dr/scripts/pveelite-down-alert.sh):

mail -s "[LEVEL] subiect" "$ALERT_RECIPIENT"      # LEVEL: INFO | WARN | CRITICAL

Se trimite alerta pentru: proces mort neasteptat, crash loop, plafon de cost atins, state.json corupt, orfani detectati la sweep.

Doua garantii care conteaza:

  • alert() nu arunca niciodata exceptii. O alerta esuata nu are voie sa doboare botul; orice eroare ajunge in alerts.log si atat.
  • Dedup 1h pe dedup_key, persistat pe disc. Fara el, un crash loop ar trimite sute de emailuri identice.

Dependinta: binarul mail. Pe LXC 171 e instalat pachetul bsd-mailx (/usr/bin/mail), iar transportul e postfix, deja prezent (/usr/sbin/sendmail). Pe o masina unde lipseste:

sudo apt-get install -y bsd-mailx

Daca mail lipseste, alertele nu se pierd: se degradeaza la scriere in ~/.claude-discord/logs/alerts.log, cu tot cu corpul mesajului, si install.sh te avertizeaza la instalare. Dar nimeni nu mai primeste nimic pe email — deci trateaza lipsa lui ca pe o defectiune, nu ca pe o optiune.

Test manual, fara sa pornesti botul:

ALERT_RECIPIENT=tu@romfast.ro python3 alerts.py INFO "test punte" "corp de test"
tail -2 ~/.claude-discord/logs/alerts.log

Cand pica

Simptom Ce faci
Botul nu raspunde deloc in Discord systemctl --user status claude-discord. Daca e failed, journalctl --user -u claude-discord -n 100. Cauza #1: token invalid sau MESSAGE CONTENT INTENT oprit.
Botul e viu dar tace, iar in log curg WSServerHandshakeError: 503 Gatewayul regional memorat ca resume_gateway_url a picat, iar discord.py il reincearca la infinit. Verifica: curl -si --http1.1 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' 'https://gateway.discord.gg/?v=10&encoding=json' — un 101 acolo plus 503 pe hostul regional confirma. Watchdogul iese singur dupa GATEWAY_WATCHDOG_S (implicit 300s) si systemd reporneste; daca vrei mai repede, systemctl --user restart claude-discord.
Botul e viu dar ignora mesajele Allowlist. Verifica DISCORD_GUILD_IDS / DISCORD_CHANNEL_IDS / DISCORD_USER_IDS din env. Respingerea e tacuta, intentionat.
Comenzile / nu apar in lista din Discord Botul a fost invitat fara scope-ul applications.commands. In log: sync de comenzi slash esuat + linkul de reinvitare. Reinvita botul, apoi systemctl --user restart claude-discord.
Unitul se invarte in restart Dupa 5 porniri esuate in 300s systemd renunta si lasa unitul failed (e voit). Repara, apoi systemctl --user reset-failed claude-discord && systemctl --user start claude-discord.
Firul e blocat pe hourglass Botul a fost restartat la mijlocul unui tur. Turul nu se reia automat (risc de dubla executie sub bypassPermissions); sweep-ul de la pornire pune un avertisment in fir. Trimite mesajul din nou.
Memoria containerului creste /cleanup (sec), apoi /cleanup force:True. Vezi si systemctl --user show claude-discord -p MemoryCurrent.
„Plafon de cost atins" E limita zilnica, nu o eroare. Pe abonament pune COST_CAP_USD_DAY=off in env si reporneste; altfel ridica valoarea sau asteapta ziua urmatoare.
Un tur pare inghetat Mesajul live arata ⏳ ruleaza de 2m30s sub unealta curenta (din tool_progress); cat timp numarul creste, turul lucreaza. Daca sta pe loc, /stop.
„Limita de utilizare atinsa" in /status Fereastra abonamentului (rate_limit_event), nu plafonul de cost. Randul utilizare arata tipul si ora resetarii; nu se poate ocoli, se asteapta.
Nu vin emailuri de alerta command -v mail; mailq; tail ~/.claude-discord/logs/alerts.log. Un NESENT in log iti spune exact de ce.
Dupa reboot serviciul nu porneste loginctl show-user claude -p Linger trebuie sa fie yes. Daca nu: sudo loginctl enable-linger claude.

Teste

cd /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge
python3 -m pytest -q                 # suita rapida, fara retea si fara Discord
python3 -m pytest -m e2e             # testele care ating CLI-ul real (lente)

Testele de ops (tests/test_alerts.py, tests/test_cleanup.py) nu trimit email real si nu omoara procese reale: folosesc un mail fals si copii de sleep pe care le pornesc si le opresc ele insele.


Securitate — ce e si ce nu e

Puntea ruleaza ca utilizatorul claude, cu --permission-mode bypassPermissions, si are exact accesul pe care il are omul in terminal: /workspace, cheile SSH, nodurile Proxmox, LXC-urile, VM-urile. Asta e functionalitate ceruta, nu scapare — puntea exista tocmai ca sa poti administra infrastructura de pe telefon.

Ce apara efectiv:

  1. Control de acces pe canal. Guild + canal + utilizator pe allowlist, gol = nimic permis. Webhook-urile si botii sunt respinsi. Contul tau de Discord devine, practic, o cheie de infrastructura — pune-i 2FA.
  2. Confirmare pentru operatiuni ireversibile. Un hook PreToolUse opreste comanda si posteaza butoane in fir; fara raspuns in fereastra de timp raspunsul e deny (fail-closed). Verificat: a blocat un rm -rf, a asteptat aprobarea externa 20s si a permis apoi executia, fara timeout. Butonul Allow (tot firul) memoreaza tiparul (regula, motiv) — nu comanda — pentru firul curent, ca o sesiune care lucreaza pe acelasi host sa nu ceara zece confirmari identice; /new, /permisiuni revoca:True si TTL-ul de 12h il sterg. Detalii in security/README.md.
  3. Wrapper infra cu lista explicita de hosturi + token Proxmox cu ACL.

Ce nu apara: regulile deny din settings. Sub bypassPermissions ele sunt un strat cosmetic — verificat, /usr/bin/ssh -V si bash -c "ssh -V" trec pe langa ele. Nu te baza pe ele ca pe o bariera.


Limitari cunoscute (asumate)

  • Nu exista jurnal de audit independent. Stratul de audit append-only pe branch dedicat a fost considerat si respins constient. Consecinta, asumata: la o problema — o comanda distructiva care a trecut, o modificare pe care nimeni nu si-o aminteste — nu exista o inregistrare independenta care sa spuna ce s-a intamplat, cand si pe ce host. Ce ramane sunt loguri care pot fi sterse de chiar procesul care le scrie: bot.log, journalctl, jurnalele de sesiune din ~/.claude/projects/ si istoricul git al repo-urilor atinse. Daca vreodata conteaza „cine si ce", asta e golul de acoperit primul.
  • Fara reluare automata a turului pierdut. Un restart la mijlocul unui tur pierde turul; nu se reia singur, fiindca sub bypassPermissions jumatate din comenzi sunt deja executate si o reluare le-ar rula a doua oara.
  • Rate limit-ul Discord e o degradare tacuta. Intervalul de editare se adapteaza (1s -> 5s), dar cand Discord franeaza nu apare niciun mesaj: raspunsul doar apare mai incet. E singura cale fara test din analiza modurilor de esec — acceptata, fiindca esecul e intarziere, nu pierdere.
  • /cleanup nu e infailibil. Domeniul lui e strict cgroup-ul serviciului, deci un proces care a iesit complet din cgroup ii scapa — asta e pretul deliberat platit ca sa nu atinga niciodata sesiunile interactive de pe container. Lista NEVER_KILL din cleanup.py protejeaza infrastructura sesiunii (systemd, sshd, tmux, code-server) — deci un proces cu un asemenea nume in linia de comanda nu va fi oprit niciodata, chiar daca e orfan.
  • Fara voce. Mesajele audio nu ajung la Claude. Imaginile si fisierele text ajung (vezi „Atasamente" mai sus); PDF, Office, arhive si celelalte tipuri sunt doar numite in prompt, nu citite.
  • Fara dashboard web — exista deja pe MoltBot, puntea nu-l duplica.
  • Un singur container. Daca LXC 171 e oprit, puntea e oprita. Nu are redundanta si nu e in HA.

Fisiere

Fisier Ce e Lane
bot.py adaptorul Discord: allowlist, comenzi, butoane, atasamente A
session_store.py state.json: scriere atomica, lock per fir, PID reuse A
runner.py proces persistent per fir, stdin JSONL, reaper A
stream.py parser tolerant de stream JSONL A
render.py chunker + loop de editare per canal A
limits.py max procese, timeout, rate limit, plafon de cost A
commands_slash.py declararea si inregistrarea comenzilor / A
config.py citeste ~/.claude-discord/env A
security/confirm_hook.py hook PreToolUse, fail-closed B
security/approvals.py canal de aprobari hook <-> bot B
security/infra wrapper cu lista de hosturi permise B
alerts.py alerte email, dedup 1h, nu arunca niciodata C
cleanup.py /cleanup: orfani, dry-run implicit C
ops/claude-discord.service unit systemd de utilizator C
ops/install.sh instalare idempotenta C
ops/env.example sablon de configurare C
ops/logrotate.conf rotatia logurilor C
dashboard/api.py dashboard de control (stare, restart, orfani, confirmari) C
dashboard/index.html, login.html, static/ interfata dashboard-ului C
dashboard/claude-discord-dashboard.service unit systemd pentru dashboard C
INTERFACES.md contractul intre module orchestrator