Implementeaza planul claude-master-plan-discord-bridge-20260830 (15 taskuri, 3 lane-uri paralele) — un bot subtire discord.py peste CLI-ul `claude`, cu proces persistent per fir alimentat pe stdin cu --input-format stream-json. Nucleu: runner (proces persistent + reaper 20min + respawn --resume), stream (parser tolerant), session_store (scriere atomica, lock per fir, detectare PID reuse, recovery), limits (max 4 procese, timeout tur, rate per user, plafon cost pe zi), render (un loop de editare per canal, interval adaptiv). Adaptor: allowlist guild/canal/user fail-closed cu respingerea webhook-urilor, comenzi !new/!cd/!model/!status/!stop/!cleanup, cost si model in subsolul fiecarui raspuns. Mesajul sosit in timpul unui tur devine steering, nu tur nou. Securitate: hook PreToolUse fail-closed care cere confirmare in Discord pentru operatiuni ireversibile, wrapper `infra` cu lista explicita de hosturi. Deny rules raman strat cosmetic, nu bariera (verificat: /usr/bin/ssh trece pe langa). Ops: alerte email pe conventia repo-ului, !cleanup pentru orfani, unit systemd user cu KillMode=control-group si limite de memorie, install.sh idempotent. Verificat: 275 teste fara retea/Discord/API (10.8s), identic cu si fara discord.py instalat; e2e pe CLI real confirma steering-ul mid-tur (mesaj la 6s intr-un tool call de 25s schimba raspunsul final). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
123 lines
4.4 KiB
Python
Executable File
123 lines
4.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""CLI `claude` fals pentru teste: citeste JSONL pe stdin, emite JSONL pe stdout.
|
|
|
|
Scenariul se alege din FAKE_CLAUDE_SCENARIO:
|
|
normal assistant + result pentru fiecare mesaj
|
|
unknown tip necunoscut + linie non-JSON inainte de assistant/result
|
|
tools tool_use + tool_result + text + result
|
|
eof se termina inainte de `result`
|
|
crash scrie pe stderr si iese cu cod 1
|
|
env raporteaza CLAUDE_DISCORD_THREAD_ID / _SESSION_ID din mediul propriu
|
|
slow "lucreaza" FAKE_CLAUDE_DELAY secunde, colecteaza mesajele venite intre timp
|
|
(asa se testeaza steering-ul mid-tur) si le enumera in `result`
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import select
|
|
import sys
|
|
import time
|
|
|
|
|
|
def emit(obj) -> None:
|
|
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def parse_args(argv):
|
|
sid, model = None, "sonnet"
|
|
for i, a in enumerate(argv):
|
|
if a == "--resume" and i + 1 < len(argv):
|
|
sid = argv[i + 1]
|
|
elif a == "--model" and i + 1 < len(argv):
|
|
model = argv[i + 1]
|
|
return sid, model
|
|
|
|
|
|
def text_of(line: str) -> str:
|
|
try:
|
|
obj = json.loads(line)
|
|
except ValueError:
|
|
return line.strip()
|
|
msg = obj.get("message") or {}
|
|
content = msg.get("content")
|
|
if isinstance(content, list):
|
|
return "".join(b.get("text", "") for b in content if isinstance(b, dict))
|
|
return str(content or "")
|
|
|
|
|
|
def assistant(sid: str, text: str) -> None:
|
|
emit({"type": "assistant", "session_id": sid,
|
|
"message": {"role": "assistant", "content": [{"type": "text", "text": text}]}})
|
|
|
|
|
|
def result(sid: str, text: str, ms: int = 10) -> None:
|
|
emit({"type": "result", "subtype": "success", "session_id": sid, "is_error": False,
|
|
"duration_ms": ms, "num_turns": 1, "result": text,
|
|
"total_cost_usd": float(os.environ.get("FAKE_CLAUDE_COST", "0.0123"))})
|
|
|
|
|
|
def drain(deadline: float) -> list[str]:
|
|
"""Citeste orice mesaj suplimentar pana la deadline (steering mid-tur)."""
|
|
extra = []
|
|
while True:
|
|
left = deadline - time.monotonic()
|
|
if left <= 0:
|
|
return extra
|
|
r, _, _ = select.select([sys.stdin], [], [], min(left, 0.05))
|
|
if r:
|
|
line = sys.stdin.readline()
|
|
if not line:
|
|
return extra
|
|
extra.append(text_of(line))
|
|
|
|
|
|
def main() -> int:
|
|
scenario = os.environ.get("FAKE_CLAUDE_SCENARIO", "normal")
|
|
sid, model = parse_args(sys.argv[1:])
|
|
sid = sid or os.environ.get("FAKE_CLAUDE_SID", "sid-fake-0001")
|
|
emit({"type": "system", "subtype": "init", "session_id": sid, "model": model,
|
|
"cwd": os.getcwd(), "tools": ["Bash", "Read"]})
|
|
if scenario == "crash":
|
|
sys.stderr.write("fake_claude: boom, ies cu 1\n")
|
|
sys.stderr.flush()
|
|
return 1
|
|
|
|
for line in sys.stdin:
|
|
prompt = text_of(line)
|
|
if scenario == "unknown":
|
|
emit({"type": "rate_limit_event", "session_id": sid, "detail": "test"})
|
|
sys.stdout.write("asta nu e JSON\n")
|
|
sys.stdout.flush()
|
|
if scenario == "tools":
|
|
emit({"type": "assistant", "session_id": sid, "message": {"role": "assistant", "content": [
|
|
{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}}]}})
|
|
emit({"type": "user", "session_id": sid, "message": {"role": "user", "content": [
|
|
{"type": "tool_result", "tool_use_id": "tu_1", "content": "fisier1\n"}]}})
|
|
if scenario == "eof":
|
|
assistant(sid, "incep si mor")
|
|
return 0
|
|
if scenario == "slow":
|
|
delay = float(os.environ.get("FAKE_CLAUDE_DELAY", "1.0"))
|
|
assistant(sid, "lucrez...")
|
|
extra = drain(time.monotonic() + delay)
|
|
msgs = [prompt] + extra
|
|
result(sid, "mesaje primite: " + " | ".join(msgs), int(delay * 1000))
|
|
continue
|
|
if scenario == "env":
|
|
assistant(sid, "raportez mediul")
|
|
result(sid, "thread={} sesiune={} marker={}".format(
|
|
os.environ.get("CLAUDE_DISCORD_THREAD_ID", "-"),
|
|
os.environ.get("CLAUDE_DISCORD_SESSION_ID", "-"),
|
|
os.environ.get("MARKER_DE_TEST", "-")))
|
|
continue
|
|
assistant(sid, f"ecou: {prompt}")
|
|
result(sid, f"ecou: {prompt}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|