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
This commit is contained in:
@@ -8,6 +8,8 @@ MODE_FILE="${SCRIPT_DIR}/current-mode"
|
||||
|
||||
# Coord API endpoint — per-machine override in identity.json (same lookup as sync.sh).
|
||||
# Falls back to the on-LAN default so existing machines keep working unchanged.
|
||||
# This script is invoked by both Claude Code (via .claude/settings.json UserPromptSubmit)
|
||||
# and Grok (via .grok/hooks/ UserPromptSubmit) for cross-driver coexistence.
|
||||
IDENTITY_PATH=""
|
||||
for candidate in "$HOME/.claude/identity.json" "${SCRIPT_DIR}/identity.json"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
|
||||
24
.claude/scripts/ensure-git-bash.ps1
Normal file
24
.claude/scripts/ensure-git-bash.ps1
Normal file
@@ -0,0 +1,24 @@
|
||||
# ensure-git-bash.ps1
|
||||
# Ensures that the 'bash' command in the current PowerShell session resolves to
|
||||
# Git for Windows / MSYS bash (required by ClaudeTools .sh scripts, vault, hooks, etc.)
|
||||
# instead of the WSL stub in WindowsApps.
|
||||
#
|
||||
# Usage:
|
||||
# . .claude/scripts/ensure-git-bash.ps1
|
||||
# Or source in profile.
|
||||
#
|
||||
# Idempotent and safe.
|
||||
|
||||
$gitBin = "C:\Program Files\Git\bin"
|
||||
$gitUsrBin = "C:\Program Files\Git\usr\bin"
|
||||
|
||||
if (Test-Path $gitBin) {
|
||||
$current = (Get-Command bash -ErrorAction SilentlyContinue).Source
|
||||
if ($current -notlike '*Git*bin*bash.exe') {
|
||||
# Remove any existing Git entries first to avoid duplicates, then prepend
|
||||
$env:Path = "$gitBin;$gitUsrBin;" + ($env:Path -replace [regex]::Escape("$gitBin;"), '' -replace [regex]::Escape("$gitUsrBin;"), '')
|
||||
Write-Verbose "[ensure-git-bash] Remapped 'bash' to Git/MSYS bash"
|
||||
}
|
||||
} else {
|
||||
Write-Warning "[ensure-git-bash] Git Bash not found at expected path $gitBin"
|
||||
}
|
||||
230
.claude/scripts/recover_grok_session.py
Normal file
230
.claude/scripts/recover_grok_session.py
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/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())
|
||||
@@ -126,9 +126,10 @@ fi
|
||||
|
||||
# --- Derive the harness profile memory dir ---------------------------------
|
||||
# Slug = absolute project path with every run of non-alphanumeric chars -> '-'.
|
||||
# Must match memory_dream.py's derivation. Prefer CLAUDE_PROJECT_DIR if set.
|
||||
# Must match memory_dream.py's derivation. Prefer CLAUDE_PROJECT_DIR if set;
|
||||
# also honor GROK_WORKSPACE_ROOT for Grok driver hooks (coexistence).
|
||||
HOME_DIR="${HOME:-$(cd ~ 2>/dev/null && pwd)}"
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$ROOT}"
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${GROK_WORKSPACE_ROOT:-$ROOT}}"
|
||||
|
||||
# Absolutize PROJECT_DIR.
|
||||
case "$PROJECT_DIR" in
|
||||
|
||||
Reference in New Issue
Block a user