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:
@@ -71,6 +71,22 @@ This file is gitignored (machine-local). The `UserPromptSubmit` hook reads it to
|
||||
|
||||
**Windows/Git Bash:** always use the relative path above (or forward slashes — `/d/claudetools/.claude/current-mode`). NEVER a backslashed Windows path like `D:\claudetools\.claude\current-mode`: Git Bash strips the backslashes and substitutes the illegal `:` with a Unicode PUA char, creating a garbled junk file instead of writing the path. A `PreToolUse(Bash)` hook (`.claude/hooks/block-backslash-winpath.sh`) blocks such redirects; `sync.sh` also strips any that slip through before staging.
|
||||
|
||||
**Windows bash command (the `bash` executable):** In PowerShell contexts (including the Grok/Claude tool run_terminal_command), `bash` often resolves to the WSL stub (`WindowsApps\bash.exe`) instead of the required Git for Windows/MSYS bash. This breaks vault.sh, sync.sh, hooks, etc.
|
||||
|
||||
Fix (idempotent):
|
||||
```powershell
|
||||
$gitBin = "C:\Program Files\Git\bin"
|
||||
$gitUsrBin = "C:\Program Files\Git\usr\bin"
|
||||
if ((Test-Path $gitBin) -and ((Get-Command bash -ErrorAction SilentlyContinue).Source -notlike '*Git*bin*bash.exe')) {
|
||||
$env:Path = "$gitBin;$gitUsrBin;" + ($env:Path -replace [regex]::Escape("$gitBin;"), '' -replace [regex]::Escape("$gitUsrBin;"), '')
|
||||
}
|
||||
```
|
||||
Then plain `bash .claude/scripts/vault.sh ...` works and shows the MSYS version.
|
||||
|
||||
Project helper: `. .claude/scripts/ensure-git-bash.ps1` (see that file + `.claude/memory/feedback_windows_bash_mapping.md`).
|
||||
|
||||
The user's PowerShell `$PROFILE` auto-applies the remap on new sessions. For critical calls, prefer the full path `"C:\Program Files\Git\bin\bash.exe" .claude/scripts/...` if env is uncertain. Git Bash terminals (direct launch) are already correct. Related: always use system OpenSSH, not Git's.
|
||||
|
||||
**Auto-initialization:** If `.claude/current-mode` is missing (e.g., fresh clone), the UserPromptSubmit hook automatically creates it with "general" as the default mode. No manual setup required.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Claude Code Commands
|
||||
# Claude Code Commands (also available to Grok)
|
||||
|
||||
Custom commands that extend Claude Code's capabilities.
|
||||
Custom commands that extend the AI's capabilities (Claude Code or Grok).
|
||||
|
||||
These live in `.claude/commands/*.md`. For Grok coexistence, thin native wrappers exist in `.grok/skills/<name>/SKILL.md` (with `name:` frontmatter so `/save`, `/rmm` etc. work as first-class Grok slash commands via project skill discovery). The wrappers delegate to these files (read them at runtime + adapt tool names). Single source of truth remains here. See `.grok/README.md` for details.
|
||||
|
||||
## Available Commands
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
Reconstruct a session log from a Claude Code transcript when a session crashed or was closed before `/save`.
|
||||
Reconstruct a session log from a Claude Code or Grok transcript when a session crashed or was closed before `/save`.
|
||||
|
||||
Claude Code writes every session live to a transcript JSONL under `~/.claude/projects/<slug>/<uuid>.jsonl`. `/recover` distills one of those transcripts back into a normal session log in the `.claude/commands/save.md` format. This is the **manual, reviewed** path; the background detector (`detect_orphaned_sessions.py`) handles unattended auto-recovery.
|
||||
- Claude Code: `~/.claude/projects/<slug>/<uuid>.jsonl`
|
||||
- Grok: `~/.grok/sessions/<slug>/<uuid>/` (chat_history.jsonl + events + terminal/call-*.log etc.)
|
||||
|
||||
`/recover` distills the transcript back into a normal ClaudeTools session log (the format used by `/save`). This is the **manual, reviewed** path. The driver is auto-detected or can be forced.
|
||||
|
||||
---
|
||||
|
||||
@@ -82,3 +85,24 @@ Use `/recover` when you know a specific session was lost and want a clean log. L
|
||||
- `--auto` and `--json` modes on `recover_session.py` exist for the detector and for scripting; `/recover` uses `--print` so Claude always reviews before anything lands on disk.
|
||||
- The prose is Ollama-drafted from the transcript; the Commands/Config/Reference sections are extracted verbatim by Python. Never trust the prose for exact commands, IPs, credentials, paths, SHAs, or ticket IDs — read those from the verbatim sections.
|
||||
- Transcripts are per-machine. You can only recover sessions that ran on the machine you are on.
|
||||
|
||||
## Grok sessions
|
||||
|
||||
Grok stores richer per-session artifacts (separate chat_history, events, per-tool terminal logs with full output, summary, etc.) under `~/.grok/sessions/<slug>/<uuid>/`.
|
||||
|
||||
The companion script is `.claude/scripts/recover_grok_session.py`.
|
||||
|
||||
Current support (MVP):
|
||||
- `--list`, `--latest`, `--uuid <id>` work and locate the right dir using the same slug rules Grok uses.
|
||||
- Extracts terminal command logs (highest value verbatim evidence) + basic metadata.
|
||||
- `--print` and `--auto` (writes under `session-logs/grok-session-....md`).
|
||||
|
||||
Full parity with the Claude recover (conversation reconstruction + Ollama prose for Summary/Decisions + exact file-edit extraction) is planned; the two scripts can share helpers later.
|
||||
|
||||
In `/recover` flows, if the chosen orphan or explicit id lives under a Grok sessions tree, the Grok script is used automatically (future enhancement to the dispatcher in recover_session.py or a thin wrapper).
|
||||
|
||||
Example (manual):
|
||||
```bash
|
||||
py .claude/scripts/recover_grok_session.py --latest --print
|
||||
py .claude/scripts/recover_grok_session.py --uuid 019e8b67-f97e-7b33-9c45-ec34b342d3eb --auto
|
||||
```
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
# PreToolUse(Bash) hook: block bash commands that REDIRECT/WRITE to a backslashed
|
||||
# Windows drive path (e.g. `echo x > D:\claudetools\.claude\current-mode`).
|
||||
#
|
||||
# Dual-driver: works when invoked from Claude Code (.claude/settings.json) or
|
||||
# from Grok (.grok/hooks/*.json PreToolUse). The caller normalizes stdin JSON
|
||||
# shape (tool_input vs toolInput) and the script emits Grok decision JSON when
|
||||
# it detects a Grok-shaped event and needs to deny.
|
||||
#
|
||||
# Why: under Git Bash / MSYS, a backslash is an escape char. `> D:\foo\bar`
|
||||
# strips the backslashes and substitutes the illegal ':' with the Unicode
|
||||
# Private-Use char U+F03A, creating a garbled junk file in the CWD instead of
|
||||
@@ -12,7 +17,25 @@
|
||||
# `icacls "D:\Homes"` or `pwsh -File C:\x.ps1` are NOT redirects, so they pass.
|
||||
|
||||
input=$(cat)
|
||||
cmd=$(echo "$input" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
||||
# Support both Claude (tool_input / tool_input.command) and Grok (toolInput / toolInput.command + hookEventName) event shapes.
|
||||
# Prefer jq; fallback to python (avoids repeated "jq: command not found" or parse errors if jq missing in env).
|
||||
cmd=$(echo "$input" | jq -r '(.toolInput // .tool_input // {}) | .command // ""' 2>/dev/null || python -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
ti = d.get('toolInput') or d.get('tool_input') or {}
|
||||
print(ti.get('command', ''))
|
||||
except:
|
||||
print('')
|
||||
" 2>/dev/null || echo '')
|
||||
is_grok=$(echo "$input" | jq -r 'if has("hookEventName") or has("toolInput") then "1" else "0" end' 2>/dev/null || python -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
print('1' if ('hookEventName' in d or 'toolInput' in d) else '0')
|
||||
except:
|
||||
print('0')
|
||||
" 2>/dev/null || echo '0')
|
||||
|
||||
# Strip quoted substrings first, so the pattern appearing INSIDE a string or a
|
||||
# commit message (e.g. git commit -m "... > D:\\path ...") does not false-trigger.
|
||||
@@ -22,6 +45,7 @@ bare=$(printf '%s' "$cmd" | sed -E "s/'[^']*'//g; s/\"[^\"]*\"//g")
|
||||
|
||||
# Block when, after quote-stripping, a redirect/tee writes to a bareword X:\ path.
|
||||
if printf '%s' "$bare" | grep -qiP '(>>?|tee)\s*[A-Za-z]:\\'; then
|
||||
reason="Blocked redirect/write to backslashed Windows path in bash (Git Bash would garble it via PUA substitution)."
|
||||
echo "BLOCKED: do not redirect/write to a backslashed Windows path in bash."
|
||||
echo ""
|
||||
echo "Git Bash strips the backslashes and PUA-substitutes ':', creating a"
|
||||
@@ -31,6 +55,10 @@ if printf '%s' "$bare" | grep -qiP '(>>?|tee)\s*[A-Za-z]:\\'; then
|
||||
echo " - relative path: echo dev > .claude/current-mode"
|
||||
echo " - MSYS forward-slash: echo dev > /d/claudetools/.claude/current-mode"
|
||||
echo " - drive forward-slash: echo dev > D:/claudetools/.claude/current-mode"
|
||||
if [ "$is_grok" = "1" ]; then
|
||||
# Emit Grok PreToolUse decision format so the hook runner can deny cleanly.
|
||||
printf '{"decision":"deny","reason":"%s"}\n' "$reason"
|
||||
fi
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
- [1Password — always use service token](feedback_1password_service_token.md) — Source OP_SERVICE_ACCOUNT_TOKEN from SOPS for every `op` call. Desktop-app integration prompts are unacceptable in agent flows.
|
||||
- [Point vault-access teammates at SOPS path](feedback_vault_pointer_for_teammates.md) — When relaying infra/credential info to Howard or other vault-access teammates, hand over the SOPS path + key anchors; don't transcribe the entry's fields into the message.
|
||||
- [/tmp path mismatch on Windows](feedback_tmp_path_windows.md) — Write tool and Git Bash resolve `/tmp` to DIFFERENT real dirs. Use heredoc or workspace path for JSON payloads handed to curl.
|
||||
- [Windows bash command mapping](feedback_windows_bash_mapping.md) — `bash` often resolves to WSL stub instead of Git/MSYS bash required by the harness. Fix by prepending `C:\Program Files\Git\bin` (and usr\bin) to PATH, or source `.claude/scripts/ensure-git-bash.ps1`. Profile has the logic; use plain `bash .claude/scripts/...` after remap. See the helper and this memory file for details.
|
||||
- [SQL instance role — verify by connections, not name](feedback_sql_instance_role_by_connection.md) — Standard installed under default `SQLEXPRESS` instance name is real. Prove role with `sys.dm_exec_sessions` + `Get-NetTCPConnection -OwningProcess` before recommending stop/uninstall.
|
||||
- [Clear-RecycleBin fails silently as SYSTEM](feedback_clear_recyclebin_system_context.md) — RMM-dispatched cleanup scripts cannot use `Clear-RecycleBin -Force`; the cmdlet uses Shell COM and silently no-ops without an interactive desktop. Enumerate `C:\$Recycle.Bin\<SID>\*` directly.
|
||||
- [Graph CA policy reads are eventually consistent](feedback_graph_ca_policy_eventual_consistency.md) — After PATCHing a CA policy (204), wait ~5s before GET-verifying; immediate reads can be stale.
|
||||
|
||||
54
.claude/memory/feedback_windows_bash_mapping.md
Normal file
54
.claude/memory/feedback_windows_bash_mapping.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Windows Bash Command Mapping (Git Bash vs WSL stub)
|
||||
|
||||
## The Issue
|
||||
On Windows machines (especially when the AI tool runs in PowerShell context), the `bash` command often resolves to the WSL stub at `C:\Users\...\WindowsApps\bash.exe` instead of the required Git for Windows / MSYS bash.
|
||||
|
||||
This breaks the entire ClaudeTools harness:
|
||||
- `.claude/scripts/vault.sh`, `sync.sh`, `check-messages.sh`, etc. expect GNU bash features, `/d/claudetools` style paths, etc.
|
||||
- Hooks and commands invoke `bash -c '...'`.
|
||||
- `self-check` expects: `bash present (GNU bash, version 5.2.37(1)-release (x86_64-pc-msys))`
|
||||
|
||||
Symptoms: "Windows Subsystem for Linux has no installed distributions." or scripts failing with path/quoting issues.
|
||||
|
||||
## The Fix
|
||||
Prepend Git Bash directories to `$env:Path` so plain `bash` resolves to the correct one:
|
||||
|
||||
```powershell
|
||||
$gitBin = "C:\Program Files\Git\bin"
|
||||
$gitUsrBin = "C:\Program Files\Git\usr\bin"
|
||||
if ((Test-Path $gitBin) -and ((Get-Command bash -ErrorAction SilentlyContinue).Source -notlike '*Git*bin*bash.exe')) {
|
||||
$env:Path = "$gitBin;$gitUsrBin;" + ($env:Path -replace [regex]::Escape("$gitBin;"), '' -replace [regex]::Escape("$gitUsrBin;"), '')
|
||||
}
|
||||
```
|
||||
|
||||
After this:
|
||||
- `Get-Command bash` returns `C:\Program Files\Git\bin\bash.exe`
|
||||
- `bash --version` shows the MSYS version
|
||||
- Plain invocations like `bash .claude/scripts/vault.sh list` work.
|
||||
|
||||
## Project Helper
|
||||
Use the reusable script:
|
||||
|
||||
```powershell
|
||||
. .claude/scripts/ensure-git-bash.ps1
|
||||
```
|
||||
|
||||
It contains the logic above (idempotent, with verbose/warning).
|
||||
|
||||
## Persistence
|
||||
The user's PowerShell profile (`$PROFILE`) sources/contains the remap logic so it applies on shell startup.
|
||||
|
||||
See: `.claude/scripts/ensure-git-bash.ps1` (the helper) and the profile for the exact code.
|
||||
|
||||
## For AI Sessions (Grok or Claude)
|
||||
- At the start of any command involving bash/harness scripts, ensure the mapping (source the ps1 or run the snippet).
|
||||
- In Grok: the `.grok/skills/windows-bash/SKILL.md` (if present) and this memory entry will be available.
|
||||
- Always prefer explicit full path ` "C:\Program Files\Git\bin\bash.exe" .claude/scripts/vault.sh ... ` for critical one-off calls if the env isn't trusted yet.
|
||||
- Git Bash terminals (launched directly) already have the correct bash.
|
||||
|
||||
## Related
|
||||
- See the backslash hook for another Git Bash gotcha: `.claude/hooks/block-backslash-winpath.sh`
|
||||
- SSH: always use system OpenSSH (`C:\Windows\System32\OpenSSH\ssh.exe`), never Git's ssh.
|
||||
- Profile and PATH ordering is the standard way; do not remove the WindowsApps stub.
|
||||
|
||||
This was fixed during a session on GURU-5070 to make Grok tool calls reliable with the harness. Add similar entries for other machine-specific env quirks.
|
||||
@@ -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
|
||||
|
||||
@@ -43,11 +43,17 @@
|
||||
".claude/scripts/check-messages.sh",
|
||||
".claude/scripts/migrate-identity.sh"
|
||||
],
|
||||
"grok_recovery_scripts": [
|
||||
".claude/scripts/recover_grok_session.py"
|
||||
],
|
||||
|
||||
"required_hook_files": [
|
||||
".claude/hooks/block-backslash-winpath.sh",
|
||||
".claude/hooks/post-commit.template"
|
||||
],
|
||||
"grok_hook_files": [
|
||||
".grok/hooks/claudetools.json"
|
||||
],
|
||||
|
||||
"required_settings_hooks": [
|
||||
{ "event": "PreToolUse", "matcher": "Bash", "command_contains": "block-backslash-winpath.sh", "why": "blocks garbled backslash Windows-path redirects in Git Bash" },
|
||||
|
||||
Reference in New Issue
Block a user