555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
300 lines
9.0 KiB
Python
300 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Import memories from competing AI tool configuration files into mem0.
|
|
|
|
Sub-commands (via sys.argv[1]):
|
|
cursorrules [--path .cursorrules]
|
|
copilot [--path .github/copilot-instructions.md]
|
|
cline [--path memory-bank/]
|
|
continue [--path .continue/rules.md]
|
|
|
|
Each sub-command reads configuration files from competing tools,
|
|
splits them into chunks, and POSTs each chunk to the mem0 API as a
|
|
project_profile memory.
|
|
|
|
Output: progress messages to stdout, errors to stderr
|
|
Exit: 0 always
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from _chunking import (
|
|
filter_and_truncate,
|
|
split_by_headers,
|
|
split_by_hr_or_headers,
|
|
)
|
|
from _identity import resolve_api_key, resolve_user_id
|
|
from _project import resolve_branch, resolve_project_id
|
|
|
|
API_URL = "https://api.mem0.ai"
|
|
HASH_STORE = os.path.expanduser("~/.mem0/import_hashes.json")
|
|
|
|
|
|
def _load_hashes() -> dict[str, str]:
|
|
if not os.path.isfile(HASH_STORE):
|
|
return {}
|
|
try:
|
|
with open(HASH_STORE) as f:
|
|
return json.load(f)
|
|
except (OSError, json.JSONDecodeError):
|
|
return {}
|
|
|
|
|
|
def _save_hashes(hashes: dict[str, str]) -> None:
|
|
os.makedirs(os.path.dirname(HASH_STORE), exist_ok=True)
|
|
try:
|
|
with open(HASH_STORE, "w") as f:
|
|
json.dump(hashes, f, indent=2)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _content_hash(content: str) -> str:
|
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def post_memory(api_key: str, content: str, user_id: str, project_id: str, branch: str, source: str) -> bool:
|
|
"""POST a single memory chunk to the mem0 API."""
|
|
metadata: dict = {
|
|
"type": "project_profile",
|
|
"source": source,
|
|
}
|
|
if branch:
|
|
metadata["branch"] = branch
|
|
|
|
body = {
|
|
"messages": [{"role": "user", "content": content}],
|
|
"user_id": user_id,
|
|
"app_id": project_id,
|
|
"metadata": metadata,
|
|
"infer": False,
|
|
}
|
|
data = json.dumps(body).encode("utf-8")
|
|
req = urllib.request.Request(
|
|
f"{API_URL}/v3/memories/add/",
|
|
data=data,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"Authorization": f"Token {api_key}",
|
|
},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
return resp.status in (200, 201)
|
|
except urllib.error.URLError as e:
|
|
print(f" [warn] API call failed: {e}", file=sys.stderr)
|
|
return False
|
|
|
|
|
|
def import_chunks(chunks: list[str], api_key: str, user_id: str, project_id: str, branch: str, source: str, hash_key: str = "") -> int:
|
|
"""Import a list of content chunks; return number of successful imports.
|
|
|
|
Skips import if content hash matches a previous run for the same hash_key."""
|
|
if hash_key:
|
|
combined = "\n".join(chunks)
|
|
current_hash = _content_hash(combined)
|
|
hashes = _load_hashes()
|
|
if hashes.get(hash_key) == current_hash:
|
|
print(f"Already imported (unchanged) -- skipping: {hash_key}")
|
|
return 0
|
|
else:
|
|
current_hash = ""
|
|
hashes = {}
|
|
|
|
success = 0
|
|
for chunk in chunks:
|
|
if post_memory(api_key, chunk, user_id, project_id, branch, source):
|
|
success += 1
|
|
|
|
if success > 0 and hash_key and current_hash:
|
|
hashes[hash_key] = current_hash
|
|
_save_hashes(hashes)
|
|
|
|
return success
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sub-command implementations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _parse_path_arg(args: list[str], flag: str, default: str) -> str:
|
|
"""Extract --path <value> from args list, falling back to default."""
|
|
for i, arg in enumerate(args):
|
|
if arg == flag and i + 1 < len(args):
|
|
return args[i + 1]
|
|
if arg.startswith(f"{flag}="):
|
|
return arg[len(flag) + 1:]
|
|
return default
|
|
|
|
|
|
def cmd_cursorrules(args: list[str]) -> None:
|
|
path = _parse_path_arg(args, "--path", ".cursorrules")
|
|
source = "cursor-import"
|
|
|
|
api_key = resolve_api_key()
|
|
user_id = resolve_user_id()
|
|
project_id = resolve_project_id()
|
|
branch = resolve_branch()
|
|
|
|
if not api_key:
|
|
print("Error: MEM0_API_KEY not set", file=sys.stderr)
|
|
return
|
|
|
|
if not os.path.isfile(path):
|
|
print(f"File not found: {path}", file=sys.stderr)
|
|
return
|
|
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
|
content = f.read()
|
|
|
|
raw_chunks = split_by_headers(content, "## ")
|
|
# Fall back to treating the whole file as one chunk if no headers found
|
|
if not raw_chunks:
|
|
raw_chunks = [content.strip()] if content.strip() else []
|
|
|
|
chunks = filter_and_truncate(raw_chunks)
|
|
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
|
|
print(f"Imported {n} memories from {source} ({path})")
|
|
|
|
|
|
def cmd_copilot(args: list[str]) -> None:
|
|
path = _parse_path_arg(args, "--path", ".github/copilot-instructions.md")
|
|
source = "copilot-import"
|
|
|
|
api_key = resolve_api_key()
|
|
user_id = resolve_user_id()
|
|
project_id = resolve_project_id()
|
|
branch = resolve_branch()
|
|
|
|
if not api_key:
|
|
print("Error: MEM0_API_KEY not set", file=sys.stderr)
|
|
return
|
|
|
|
if not os.path.isfile(path):
|
|
print(f"File not found: {path}", file=sys.stderr)
|
|
return
|
|
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
|
content = f.read()
|
|
|
|
raw_chunks = split_by_headers(content, "## ")
|
|
if not raw_chunks:
|
|
raw_chunks = [content.strip()] if content.strip() else []
|
|
|
|
chunks = filter_and_truncate(raw_chunks)
|
|
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
|
|
print(f"Imported {n} memories from {source} ({path})")
|
|
|
|
|
|
def cmd_cline(args: list[str]) -> None:
|
|
dir_path = _parse_path_arg(args, "--path", "memory-bank/")
|
|
source = "cline-import"
|
|
|
|
api_key = resolve_api_key()
|
|
user_id = resolve_user_id()
|
|
project_id = resolve_project_id()
|
|
branch = resolve_branch()
|
|
|
|
if not api_key:
|
|
print("Error: MEM0_API_KEY not set", file=sys.stderr)
|
|
return
|
|
|
|
if not os.path.isdir(dir_path):
|
|
print(f"Directory not found: {dir_path}", file=sys.stderr)
|
|
return
|
|
|
|
md_files = sorted(
|
|
f for f in os.listdir(dir_path) if f.endswith(".md")
|
|
)
|
|
if not md_files:
|
|
print(f"No .md files found in {dir_path}", file=sys.stderr)
|
|
return
|
|
|
|
total = 0
|
|
for filename in md_files:
|
|
filepath = os.path.join(dir_path, filename)
|
|
with open(filepath, encoding="utf-8", errors="replace") as f:
|
|
content = f.read().strip()
|
|
if not content:
|
|
continue
|
|
chunks = filter_and_truncate([content])
|
|
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{filepath}")
|
|
total += n
|
|
|
|
print(f"Imported {total} memories from {source} ({dir_path})")
|
|
|
|
|
|
def cmd_continue(args: list[str]) -> None:
|
|
path = _parse_path_arg(args, "--path", ".continue/rules.md")
|
|
source = "continue-import"
|
|
|
|
api_key = resolve_api_key()
|
|
user_id = resolve_user_id()
|
|
project_id = resolve_project_id()
|
|
branch = resolve_branch()
|
|
|
|
if not api_key:
|
|
print("Error: MEM0_API_KEY not set", file=sys.stderr)
|
|
return
|
|
|
|
if not os.path.isfile(path):
|
|
print(f"File not found: {path}", file=sys.stderr)
|
|
return
|
|
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
|
content = f.read()
|
|
|
|
raw_chunks = split_by_hr_or_headers(content)
|
|
if not raw_chunks:
|
|
raw_chunks = [content.strip()] if content.strip() else []
|
|
|
|
chunks = filter_and_truncate(raw_chunks)
|
|
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
|
|
print(f"Imported {n} memories from {source} ({path})")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
COMMANDS = {
|
|
"cursorrules": cmd_cursorrules,
|
|
"copilot": cmd_copilot,
|
|
"cline": cmd_cline,
|
|
"continue": cmd_continue,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
|
available = ", ".join(COMMANDS.keys())
|
|
print("Usage: import_competing_tools.py <subcommand> [--path <path>]", file=sys.stderr)
|
|
print(f"Subcommands: {available}", file=sys.stderr)
|
|
sys.exit(0)
|
|
|
|
subcommand = sys.argv[1]
|
|
remaining_args = sys.argv[2:]
|
|
COMMANDS[subcommand](remaining_args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
sys.exit(0)
|