mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
5973f9b787
Signed-off-by: phernandez <paul@basicmachines.co>
233 lines
6.7 KiB
Python
Executable File
233 lines
6.7 KiB
Python
Executable File
#!/usr/bin/env -S uv run --script
|
|
"""Checkpoint Codex work into Basic Memory before compaction."""
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
UUID_RE = re.compile(
|
|
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def basic_memory_command() -> list[str] | None:
|
|
configured = os.environ.get("BM_BIN")
|
|
if configured:
|
|
return shlex.split(configured)
|
|
if shutil.which("basic-memory"):
|
|
return ["basic-memory"]
|
|
if shutil.which("bm"):
|
|
return ["bm"]
|
|
if shutil.which("uvx"):
|
|
return ["uvx", "basic-memory"]
|
|
if shutil.which("uv"):
|
|
return ["uv", "tool", "run", "basic-memory"]
|
|
return None
|
|
|
|
|
|
def parse_payload() -> dict:
|
|
try:
|
|
payload = json.loads(sys.stdin.read() or "{}")
|
|
except Exception:
|
|
return {}
|
|
return payload if isinstance(payload, dict) else {}
|
|
|
|
|
|
def load_config(directory: Path) -> dict:
|
|
path = directory / ".codex" / "basic-memory.json"
|
|
try:
|
|
data = json.loads(path.read_text())
|
|
except Exception:
|
|
return {}
|
|
if not isinstance(data, dict):
|
|
return {}
|
|
return data.get("basicMemory", data)
|
|
|
|
|
|
def text_of(content):
|
|
if isinstance(content, str):
|
|
return content
|
|
if isinstance(content, list):
|
|
parts = []
|
|
for block in content:
|
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
text = block.get("text")
|
|
if isinstance(text, str):
|
|
parts.append(text)
|
|
return "\n".join(parts)
|
|
return ""
|
|
|
|
|
|
def transcript_turns(path: str):
|
|
collected = []
|
|
if not path:
|
|
return collected
|
|
try:
|
|
with open(path) as handle:
|
|
for line in handle:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
obj = json.loads(line)
|
|
except Exception:
|
|
continue
|
|
if obj.get("isMeta") or obj.get("toolUseResult") is not None:
|
|
continue
|
|
msg = obj.get("message") if isinstance(obj.get("message"), dict) else obj
|
|
role = msg.get("role") or obj.get("type")
|
|
if role not in ("user", "assistant"):
|
|
continue
|
|
text = text_of(msg.get("content")).strip()
|
|
if text:
|
|
collected.append((role, text))
|
|
except Exception:
|
|
return []
|
|
return collected
|
|
|
|
|
|
def git_status(directory: Path) -> list[str]:
|
|
try:
|
|
out = subprocess.run(
|
|
["git", "status", "--short"],
|
|
cwd=directory,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
except Exception:
|
|
return []
|
|
if out.returncode != 0:
|
|
return []
|
|
return [line for line in out.stdout.splitlines() if line.strip()][:20]
|
|
|
|
|
|
def clip(value: str, limit: int) -> str:
|
|
compact = " ".join(value.split())
|
|
return compact if len(compact) <= limit else compact[: limit - 1].rstrip() + "..."
|
|
|
|
|
|
def main() -> int:
|
|
bm_cmd = basic_memory_command()
|
|
if not bm_cmd:
|
|
return 0
|
|
|
|
payload = parse_payload()
|
|
cwd = Path(payload.get("cwd") or os.getcwd())
|
|
transcript_path = payload.get("transcript_path") or ""
|
|
session_id = payload.get("session_id") or ""
|
|
turn_id = payload.get("turn_id") or ""
|
|
trigger = payload.get("trigger") or ""
|
|
model = payload.get("model") or ""
|
|
|
|
cfg = load_config(cwd)
|
|
primary_project = str(cfg.get("primaryProject") or "").strip()
|
|
capture_folder = str(cfg.get("captureFolder") or "codex-sessions").strip()
|
|
|
|
if not primary_project:
|
|
return 0
|
|
|
|
conversation = transcript_turns(transcript_path)
|
|
if not conversation or not any(role == "user" for role, _ in conversation):
|
|
return 0
|
|
|
|
user_messages = [text for role, text in conversation if role == "user"]
|
|
assistant_messages = [text for role, text in conversation if role == "assistant"]
|
|
opening = user_messages[0] if user_messages else ""
|
|
recent_user = user_messages[-3:]
|
|
recent_assistant = assistant_messages[-2:]
|
|
status_lines = git_status(cwd)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
iso = now.isoformat(timespec="seconds")
|
|
title = f"Codex session {now.strftime('%Y-%m-%d %H:%M:%S')} - {clip(opening, 40)}"
|
|
|
|
frontmatter = [
|
|
"---",
|
|
"type: codex_session",
|
|
"status: open",
|
|
f"started: {iso}",
|
|
f"ended: {iso}",
|
|
f"project: {primary_project}",
|
|
f"cwd: {cwd}",
|
|
]
|
|
if session_id:
|
|
frontmatter.append(f"codex_session_id: {session_id}")
|
|
if turn_id:
|
|
frontmatter.append(f"codex_turn_id: {turn_id}")
|
|
if trigger:
|
|
frontmatter.append(f"trigger: {trigger}")
|
|
if model:
|
|
frontmatter.append(f"model: {model}")
|
|
frontmatter += ["capture: extractive", "---"]
|
|
|
|
body = [
|
|
"",
|
|
f"# {title}",
|
|
"",
|
|
"_Automatic Codex pre-compaction checkpoint. It records the working cursor, "
|
|
"not a polished summary._",
|
|
"",
|
|
"## Summary",
|
|
f"Working in `{cwd}`.",
|
|
f"- Opening request: {clip(opening, 300)}" if opening else "",
|
|
"",
|
|
"## Recent User Cursor",
|
|
]
|
|
body += [f"- {clip(message, 240)}" for message in recent_user]
|
|
if recent_assistant:
|
|
body += ["", "## Recent Assistant Notes"]
|
|
body += [f"- {clip(message, 240)}" for message in recent_assistant]
|
|
if status_lines:
|
|
body += ["", "## Working Tree"]
|
|
body += [f"- `{line}`" for line in status_lines]
|
|
body += [
|
|
"",
|
|
"## Observations",
|
|
f"- [context] Codex worked in `{cwd}`",
|
|
f"- [context] Session opened with: {clip(opening, 200)}" if opening else "",
|
|
"- [next_step] Re-read this checkpoint, inspect the current worktree, and "
|
|
"continue from the latest user request",
|
|
]
|
|
|
|
content = "\n".join(frontmatter + body)
|
|
project_flag = "--project-id" if UUID_RE.match(primary_project) else "--project"
|
|
|
|
try:
|
|
subprocess.run(
|
|
[
|
|
*bm_cmd,
|
|
"tool",
|
|
"write-note",
|
|
"--title",
|
|
title,
|
|
"--folder",
|
|
capture_folder,
|
|
project_flag,
|
|
primary_project,
|
|
"--tags",
|
|
"codex",
|
|
"--tags",
|
|
"auto-capture",
|
|
],
|
|
input=content,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
except Exception:
|
|
return 0
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|