Files
claudetools/.claude/scripts/recover_grok_session.py
Mike Swanson 446a6c1b1c sync: auto-sync from GURU-5070 at 2026-06-02 20:40:54
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-02 20:40:54
2026-06-02 20:40:58 -07:00

231 lines
9.4 KiB
Python

#!/usr/bin/env python3
"""
recover_grok_session.py -- reconstruct a ClaudeTools session log from a Grok session.
Grok stores per-workspace sessions under:
~/.grok/sessions/<slug>/ (slug = URL-encoded path, e.g. D%3A%5Cclaudetools)
~/.grok/sessions/<slug>/<uuid>/ (one dir per actual session)
- chat_history.jsonl, events.jsonl, updates.jsonl
- terminal/ (call-*.log files with exact command + output for run_terminal_cmd etc.)
- summary.json, system_prompt.txt, prompt_context.json, etc.
This is the Grok counterpart to recover_session.py (which targets Claude Code's
~/.claude/projects/<slug>/<uuid>.jsonl transcripts).
The goal is the same: produce a markdown session log in the format expected by
.claude/commands/save.md (## User, ## Summary, Key Decisions, Commands & Outputs,
File Changes, etc.) so that /save, wiki-compile, memory, etc. work uniformly
regardless of which driver (Claude Code or Grok) was used for the session.
Status: skeleton / MVP. Extracts terminal commands + outputs (the most valuable
verbatim evidence) and basic session metadata. Full chat reconstruction + Ollama
prose drafting (like the Claude recover) can be added by sharing code with
recover_session.py.
Usage (from repo root):
python .claude/scripts/recover_grok_session.py --list
python .claude/scripts/recover_grok_session.py --latest --print
python .claude/scripts/recover_grok_session.py --uuid 019e8b67-f97e-7b33-9c45-ec34b342d3eb --auto
The /recover command will be extended to dispatch to this for Grok sessions.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Tuple
# --- Resolution of Grok home and current workspace slug (mirrors how Grok does it) ---
def get_grok_home() -> Path:
if os.name == "nt":
return Path(os.environ.get("USERPROFILE", "~")).expanduser() / ".grok"
return Path(os.environ.get("HOME", "~")).expanduser() / ".grok"
def get_workspace_slug(cwd: Optional[Path] = None) -> str:
"""Mimic Grok's slug for the sessions/<slug> directory.
Grok uses URL-style encoding of the absolute path ( : -> %3A, \\ -> %5C, / -> %2F ).
"""
root = (cwd or Path.cwd()).resolve()
# On Windows the path has drive letter + backslashes.
s = str(root)
# Percent-encode unsafe for dir name (Grok appears to use % encoding).
# Simple approach that matched observed dirs: replace special with %HH
encoded = re.sub(r'([^\w\-./])', lambda m: f"%{ord(m.group(1)):02X}", s)
# Grok observed: D%3A%5Cclaudetools (backslashes and colon encoded, no extra for / sometimes)
# But on the FS it was D%3A%5Cclaudetools
return encoded.replace("/", "%2F")
def find_session_dirs(workspace_root: Optional[Path] = None) -> List[Tuple[str, Path]]:
"""Return list of (uuid, dir_path) for sessions under the current workspace slug."""
grok_home = get_grok_home()
slug = get_workspace_slug(workspace_root)
base = grok_home / "sessions" / slug
if not base.exists():
return []
results = []
for p in base.iterdir():
if p.is_dir() and re.match(r'^[0-9a-f]{8}-', p.name): # rough UUID check
results.append((p.name, p))
results.sort(key=lambda t: t[1].stat().st_mtime, reverse=True)
return results
def get_latest_session(workspace_root: Optional[Path] = None) -> Optional[Tuple[str, Path]]:
sessions = find_session_dirs(workspace_root)
return sessions[0] if sessions else None
# --- Extraction ---
def read_jsonl(path: Path) -> List[Dict[str, Any]]:
if not path.exists():
return []
out = []
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if line:
try:
out.append(json.loads(line))
except Exception:
pass
return out
def extract_terminal_commands(session_dir: Path) -> List[Dict[str, str]]:
"""Pull exact commands + outputs from the terminal/ call logs.
These are the highest-fidelity evidence for the session log "Commands & Outputs" section.
"""
term_dir = session_dir / "terminal"
if not term_dir.exists():
return []
results = []
for f in sorted(term_dir.glob("call-*.log")):
try:
content = f.read_text(encoding="utf-8", errors="replace")
# The tool already logs the command in the filename sometimes, or inside.
# From observed usage the file contains the full command + stdout/stderr.
results.append({
"file": f.name,
"content": content[:20000] # cap per call for practicality
})
except Exception:
pass
return results
def extract_basic_metadata(session_dir: Path) -> Dict[str, Any]:
meta: Dict[str, Any] = {"uuid": session_dir.name}
# Try summary.json
sj = session_dir / "summary.json"
if sj.exists():
try:
meta["summary"] = json.loads(sj.read_text(encoding="utf-8", errors="replace"))
except Exception:
pass
# opened time from dir mtime or active_sessions if present
meta["dir_mtime"] = datetime.fromtimestamp(session_dir.stat().st_mtime, tz=timezone.utc).isoformat()
return meta
def build_session_log(uuid: str, session_dir: Path, workspace_root: Path) -> str:
"""Produce markdown in the approximate shape expected by /save and wiki tools."""
lines: List[str] = []
lines.append(f"# Grok Session Log — {uuid}")
lines.append("")
lines.append(f"**Workspace:** {workspace_root}")
lines.append(f"**Grok session dir:** {session_dir}")
lines.append(f"**Recovered at:** {datetime.now(timezone.utc).isoformat()}")
lines.append("**Driver:** Grok")
lines.append("")
meta = extract_basic_metadata(session_dir)
lines.append("## Metadata")
lines.append(json.dumps(meta, indent=2, default=str))
lines.append("")
# Terminal calls (commands + output)
calls = extract_terminal_commands(session_dir)
lines.append("## Commands & Outputs (from terminal call logs)")
if calls:
for c in calls:
lines.append(f"### {c['file']}")
lines.append("```")
lines.append(c["content"].rstrip())
lines.append("```")
lines.append("")
else:
lines.append("(no terminal call logs found)")
lines.append("")
# TODO (future): parse chat_history / events / updates for user/assistant turns,
# file edits (search_replace results), read_file contents, decisions, etc.
# Then feed prose sections (Summary, Key Decisions, Pending Tasks) to Ollama
# exactly like recover_session.py does.
lines.append("## Notes")
lines.append("This is an MVP recovery from Grok native session artifacts.")
lines.append("Full fidelity (conversation turns, structured tool calls, file changes) ")
lines.append("and Ollama-assisted summarization can be added by extending this script ")
lines.append("or sharing logic with recover_session.py.")
lines.append("Run with --auto to write a session-log/ file in the conventional location.")
lines.append("")
return "\n".join(lines)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--uuid", help="Specific Grok session UUID to recover")
ap.add_argument("--latest", action="store_true", help="Recover the most recent session for this workspace")
ap.add_argument("--list", action="store_true", help="List available Grok sessions for this workspace slug")
ap.add_argument("--print", action="store_true", help="Print the reconstructed log to stdout")
ap.add_argument("--auto", action="store_true", help="Write a session log under session-logs/ (or projects/... ) following ClaudeTools conventions")
ap.add_argument("--workspace", type=Path, default=None, help="Override workspace root for slug calculation")
args = ap.parse_args()
ws_root = (args.workspace or Path.cwd()).resolve()
if args.list:
for uuid, d in find_session_dirs(ws_root):
print(f"{uuid} {d} (mtime {datetime.fromtimestamp(d.stat().st_mtime)})")
return 0
target: Optional[Tuple[str, Path]] = None
if args.uuid:
base = get_grok_home() / "sessions" / get_workspace_slug(ws_root)
p = base / args.uuid
if p.exists():
target = (args.uuid, p)
else:
print(f"[ERROR] UUID dir not found: {p}", file=sys.stderr)
return 1
elif args.latest:
target = get_latest_session(ws_root)
if not target:
print("[ERROR] No Grok sessions found for this workspace", file=sys.stderr)
return 1
else:
print("Specify --uuid, --latest, or --list", file=sys.stderr)
return 2
uuid, sdir = target
log = build_session_log(uuid, sdir, ws_root)
if args.print or not args.auto:
print(log)
if args.auto:
# Minimal auto placement: root session-logs/ for now (real /save logic is more sophisticated).
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d")
out_dir = ws_root / "session-logs"
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / f"grok-session-{ts}-{uuid[:8]}.md"
out_path.write_text(log, encoding="utf-8")
print(f"[OK] Wrote {out_path}")
# In real use we would also update coord, create a checkpoint entry etc.
return 0
if __name__ == "__main__":
sys.exit(main())