Backup automatico script del 2026-11-01 07:00

This commit is contained in:
daniele committed 2026-11-01 07:00:03 +01:00
1 parent 8ad51325ce
commit 48c0d0c21e
11 files changed
+883 -4

No files matched your search

@@ -4,6 +4,7 @@
Fonti (massimo contesto utile, non dump grezzo):
- transcript agenti Cursor (query utente + conclusioni assistente)
- transcript Claude Code di Pi1 (locali) e Pi2 (copiati via rsync in ~/.cache/claude-transcripts/)
- audit_log tool MCP del giorno
- commit git recenti in repo tipici (rete, loogle-mcp)
- piani Cursor `~/.cursor/plans/*.plan.md` → artifacts/plans/ + indice in context.md
@@ -52,6 +53,12 @@ DEFAULT_GIT_REPOS = [
Path.home() / "docker/loogle-mcp",
]
DEFAULT_PLANS_DIR = Path.home() / ".cursor/plans"
# Claude Code: <root>/<progetto>/<sessione>.jsonl. Pi2 viene copiato qui prima della lettura.
CLAUDE_LOCAL_ROOT = Path.home() / ".claude/projects"
CLAUDE_REMOTES = [("pi2", "daniely@192.168.128.81")]
CLAUDE_CACHE = Path.home() / ".cache/claude-transcripts"
SYSTEM_TAG_RE = re.compile(r"<(system-reminder|command-[a-z-]+|local-command-[a-z-]+)[^>]*>.*?</\1>", re.DOTALL)
PASTED_TAG_RE = re.compile(r"</?pasted_content[^>]*>")
def _load_dotenv() -> None:
@@ -253,6 +260,119 @@ def collect_transcripts(day: date, roots: list[Path], max_chats: int = 25) -> li
return chats
def sync_claude_remotes(remotes: list[tuple[str, str]], cache: Path) -> list[tuple[str, Path]]:
"""Copia i transcript Claude Code dei nodi remoti in cache; un nodo giù non blocca il digest."""
roots: list[tuple[str, Path]] = []
for name, host in remotes:
dest = cache / name
dest.mkdir(parents=True, exist_ok=True)
cmd = [
"rsync", "-a", "--timeout=30", "--prune-empty-dirs",
"-e", "ssh -o BatchMode=yes -o ConnectTimeout=6",
"--include=*/", "--exclude=subagents/", "--include=*.jsonl", "--exclude=*",
f"{host}:.claude/projects/", f"{dest}/",
]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
if r.returncode != 0:
print(f"# claude {name}: rsync rc={r.returncode} {r.stderr.strip()[:200]} (uso la copia precedente)",
file=sys.stderr)
except (OSError, subprocess.TimeoutExpired) as e:
print(f"# claude {name}: rsync non riuscito: {e} (uso la copia precedente)", file=sys.stderr)
roots.append((name, dest))
return roots
def _claude_text(content: Any) -> list[str]:
if isinstance(content, str):
return [content]
out: list[str] = []
if isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get("type") == "text" and part.get("text"):
out.append(part["text"])
return out
def _clean_claude_user(text: str) -> str:
text = SYSTEM_TAG_RE.sub(" ", text)
text = PASTED_TAG_RE.sub(" ", text)
return text.strip()
def collect_claude_transcripts(day: date, roots: list[tuple[str, Path]], max_chats: int = 25) -> list[dict]:
"""Sessioni Claude Code con almeno un messaggio umano nel giorno (ora locale)."""
start, end = _day_bounds(day)
files: list[tuple[str, Path]] = []
for host, root in roots:
if root.is_dir():
files.extend((host, p) for p in root.glob("*/*.jsonl"))
chats: list[dict] = []
for host, path in sorted(files, key=lambda hp: hp[1].stat().st_mtime, reverse=True):
if datetime.fromtimestamp(path.stat().st_mtime) < start:
continue
title = ""
queries: list[str] = []
tails: list[str] = []
paths_touched: Counter[str] = Counter()
try:
with open(path, encoding="utf-8") as fh:
for line in fh:
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
kind = obj.get("type")
if kind == "custom-title":
title = obj.get("customTitle") or title
continue
if kind not in ("user", "assistant") or obj.get("isSidechain") or obj.get("isMeta"):
continue
try:
when = datetime.fromisoformat(obj["timestamp"].replace("Z", "+00:00")).astimezone().replace(tzinfo=None)
except (KeyError, ValueError):
continue
if not (start <= when < end):
continue
msg = obj.get("message") or {}
content = msg.get("content") if isinstance(msg, dict) else None
if kind == "user":
if obj.get("turnOrigin") not in (None, "human"):
continue
for t in _claude_text(content):
q = _truncate(_clean_claude_user(t), 280)
if q and not _is_noise_query(q):
queries.append(q)
else:
for t in _claude_text(content):
if len(t) > 80:
tails.append(_truncate(t, 320))
if isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get("type") == "tool_use":
inp = part.get("input") or {}
for key in ("file_path", "path"):
if isinstance(inp.get(key), str):
paths_touched[inp[key]] += 1
except OSError:
continue
if not queries:
continue
seen: set[str] = set()
uniq = [q for q in queries if not (q in seen or seen.add(q))]
chats.append({
"id": path.stem,
"source": f"claude-{host}",
"title": title,
"queries": uniq[:8],
"conclusions": tails[-3:],
"paths": [p for p, _ in paths_touched.most_common(8)],
})
if len(chats) >= max_chats:
break
return chats
def collect_audit(day: date, db_path: Path) -> dict:
if not db_path.is_file():
return {"tools": [], "total": 0}
@@ -474,15 +594,15 @@ def build_markdown(
"",
]
lines.append("### Chat / agenti Cursor")
lines.append("### Chat / agenti (Cursor + Claude Code)")
if not chats:
lines.append("- Nessun transcript rilevante per questo giorno.")
else:
lines.append(f"- Sessioni considerate: **{len(chats)}**")
for i, chat in enumerate(chats, 1):
title = chat["queries"][0] if chat["queries"] else chat["id"]
lines.append(f"{i}. **{_truncate(title, 120)}** `[{chat['id'][:8]}]`")
for q in chat["queries"][1:4]:
title = chat.get("title") or (chat["queries"][0] if chat["queries"] else chat["id"])
lines.append(f"{i}. **{_truncate(title, 120)}** `[{chat.get('source', 'cursor')} {chat['id'][:8]}]`")
for q in chat["queries"][(0 if chat.get("title") else 1):4]:
lines.append(f" - Q: {_truncate(q, 160)}")
for c in chat["conclusions"][-2:]:
lines.append(f" - → {_truncate(c, 200)}")
@@ -585,6 +705,7 @@ def main() -> int:
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--no-index", action="store_true", help="Non aggiornare Qdrant ctx_*")
parser.add_argument("--max-chats", type=int, default=20)
parser.add_argument("--no-claude", action="store_true", help="Non leggere i transcript Claude Code")
parser.add_argument(
"--no-plans",
action="store_true",
@@ -609,6 +730,9 @@ def main() -> int:
)
chats = collect_transcripts(day, DEFAULT_TRANSCRIPT_ROOTS, max_chats=args.max_chats)
if not args.no_claude:
claude_roots = [("pi1", CLAUDE_LOCAL_ROOT)] + sync_claude_remotes(CLAUDE_REMOTES, CLAUDE_CACHE)
chats += collect_claude_transcripts(day, claude_roots, max_chats=args.max_chats)
audit = collect_audit(day, Path(os.environ["MCP_DB"]))
git_lines = collect_git(day, DEFAULT_GIT_REPOS)
block = build_markdown(
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.