Backup automatico script del 2026-11-01 07:00
This commit is contained in:
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(
|
||||
|
||||