555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
151 lines
4.3 KiB
Python
151 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Parse a mem0 export file and output JSON.
|
|
|
|
Input: path to a mem0-export-*.md file (sys.argv[1])
|
|
Output: JSON array of memory records to stdout
|
|
Exit: 0 always
|
|
|
|
Each block in the file is delimited by lines containing exactly "---".
|
|
Blocks have a YAML-like frontmatter section (key: value lines) followed
|
|
by a blank line and the memory content text.
|
|
|
|
Example block format:
|
|
---
|
|
id: abc123
|
|
created_at: 2024-01-01T00:00:00Z
|
|
type: task_learnings
|
|
confidence: 0.9
|
|
branch: main
|
|
files: src/foo.py, src/bar.py
|
|
categories: coding_conventions, task_learnings
|
|
---
|
|
The actual memory content text goes here.
|
|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
|
|
def parse_blocks(content: str) -> list[dict]:
|
|
"""Split content on '---' boundaries and parse each block.
|
|
|
|
Returns a list of dicts with keys:
|
|
id, type, confidence, branch, files (list), categories (list), content (str)
|
|
|
|
Blocks with empty content are skipped.
|
|
Missing optional fields default to "" (scalar) or [] (list fields).
|
|
"""
|
|
# Normalise line endings
|
|
content = content.replace("\r\n", "\n").replace("\r", "\n")
|
|
|
|
# Split on lines that are exactly "---"
|
|
raw_blocks = re.split(r"(?m)^---\s*$", content)
|
|
|
|
# After splitting on "---", the structure for each memory is:
|
|
# raw_blocks[0] = preamble (before first ---, typically empty)
|
|
# raw_blocks[1] = frontmatter for block 1
|
|
# raw_blocks[2] = content for block 1
|
|
# raw_blocks[3] = frontmatter for block 2
|
|
# raw_blocks[4] = content for block 2
|
|
# ...
|
|
# So frontmatter blocks are at odd indices (1, 3, 5, ...) and
|
|
# content blocks at even indices (2, 4, 6, ...).
|
|
|
|
results: list[dict] = []
|
|
|
|
# Pair up frontmatter + content starting at index 1
|
|
i = 1
|
|
while i < len(raw_blocks):
|
|
frontmatter_raw = raw_blocks[i]
|
|
content_raw = raw_blocks[i + 1] if i + 1 < len(raw_blocks) else ""
|
|
|
|
# Parse the frontmatter key-value pairs
|
|
fm = _parse_frontmatter(frontmatter_raw)
|
|
|
|
# Strip leading/trailing whitespace from content
|
|
memory_content = content_raw.strip()
|
|
|
|
# Skip blocks with empty content
|
|
if not memory_content:
|
|
i += 2
|
|
continue
|
|
|
|
record = {
|
|
"id": fm.get("id", ""),
|
|
"type": fm.get("type", ""),
|
|
"confidence": fm.get("confidence", ""),
|
|
"branch": fm.get("branch", ""),
|
|
"files": _parse_list_field(fm.get("files", "")),
|
|
"categories": _parse_list_field(fm.get("categories", "")),
|
|
"content": memory_content,
|
|
}
|
|
|
|
# Include created_at if present
|
|
if "created_at" in fm:
|
|
record["created_at"] = fm["created_at"]
|
|
|
|
results.append(record)
|
|
i += 2
|
|
|
|
return results
|
|
|
|
|
|
def _parse_frontmatter(text: str) -> dict[str, str]:
|
|
"""Parse simple 'key: value' lines from frontmatter text.
|
|
|
|
Only the first colon is used as the delimiter — values may contain colons.
|
|
Lines not matching 'key: value' are ignored.
|
|
"""
|
|
result: dict[str, str] = {}
|
|
for line in text.splitlines():
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", line)
|
|
if match:
|
|
key = match.group(1).strip()
|
|
value = match.group(2).strip()
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _parse_list_field(value: str) -> list[str]:
|
|
"""Split a comma-separated value into a list, stripping whitespace.
|
|
|
|
Returns [] for empty/whitespace-only input.
|
|
"""
|
|
if not value or not value.strip():
|
|
return []
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2:
|
|
print("Usage: parse_export_file.py <path-to-export-file>", file=sys.stderr)
|
|
print("[]")
|
|
sys.exit(0)
|
|
|
|
filepath = sys.argv[1]
|
|
try:
|
|
if filepath == "-":
|
|
content = sys.stdin.read()
|
|
else:
|
|
with open(filepath, encoding="utf-8", errors="replace") as f:
|
|
content = f.read()
|
|
except OSError as e:
|
|
print(f"Error reading file: {e}", file=sys.stderr)
|
|
print("[]")
|
|
sys.exit(0)
|
|
|
|
records = parse_blocks(content)
|
|
print(json.dumps(records, ensure_ascii=False, indent=2))
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|