Skill: crimsondevil333333/memory-keeper (version 1.0.1)

--- name: memory-keeper description: Copy and snapshot all important agent context (MEMORY.md, memory/*.md, AGENTS.md, SOUL.md, etc.) into a dedicated archive directory or repo. Use this skill when you want to back up your memories, context, or configuration files in preparation for maintenance, corruption recovery, or transferring to another host. --- # Memory Keeper Memory Keeper is a simple CLI that copies the critical OpenClaw context files (daily memory logs, DESCRIPTION.md, personality documents, heartbeat reminders) into a safe archive destination and optionally commits/pushes them to a configured git repo. It keeps the same file layout so you can restore or inspect the history without grabbing the whole workspace. ## Features - **Snapshots**: Copies `memory/*.md`, `MEMORY.md`, `AGENTS.md`, `SOUL.md`, `USER.md`, `TOOLS.md`, `HEARTBEAT.md`, and optional extras into the archive path while preserving directory structure. - **Git-friendly**: If the target archive is a git repo, Memory Keeper can initialize it, create commits, and push changes to your remote branch (configurable via CLI flags). - **Portable**: Works on any platform; just point `--workspace` to a workspace root containing those files. ## Get started ```bash python3 skills/memory-keeper/scripts/memory_sync.py --target ~/clawdy-memories --commit --message "Sync up" --remote https://github.com/your-org/clawdy-memories.git --push ``` See `references/usage.md` for configuration tips, automation recipes, and a troubleshooting guide. ## Resources - **GitHub:** https://github.com/CrimsonDevil333333/memory-keeper - **ClawHub:** https://www.clawhub.ai/skills/memory-keeper

path: README.md
content: # Memory Keeper skill

Memory Keeper copies the OpenClaw memory journal files (`memory/*.md`, `MEMORY.md`, `AGENTS.md`, `SOUL.md`, `USER.md`, `TOOLS.md`, `HEARTBEAT.md`) into a dedicated archive folder or repository. Use it whenever you need a durable snapshot of your agent context before risky operations, updates, or migrations.

## Key highlights

- **Automated snapshots**: Copies the core documents plus optional globs via `--allow-extra` while keeping the workspace structure intact.
- **Git-ready**: Optionally initializes a repository inside the archive, commits the snapshot, configures a remote, and pushes the new branch.
- **Logs its own work**: Every successful run appends a timestamped entry to today’s `memory/YYYY-MM-DD.md`, so the journal records when and where the archive changed.
- **Friendly failures**: If an authentication error happens while configuring or pushing to a remote, the CLI prints a helpful reminder about tokens/SSH keys instead of crashing.

## Usage

```bash
python3 skills/memory-keeper/scripts/memory_sync.py \
  --target ~/clawdy-memories --commit --message "Post-session" --remote https://github.com/your-org/clawdy-memories.git --push
```

Pass `--skip-memory` when you only want the top-level metadata files, or `--allow-extra extra-notes.md` to snapshot additional assets.

## Logging & tokens

Memory Keeper now writes a bullet to `memory/YYYY-MM-DD.md` after each successful run. Look there for entries like:

```
- [2026-02-03 12:00:00 IST] Memory Keeper synced to /path/to/archive (commit=True, push=False, remote=https://...)
```

If a git command fails because the remote needs a token, the CLI will report `Git command 'git -C ... <args>' failed` and remind you to configure your credential helper or embed the token/SSH key in the URL. This keeps the tool readable even when authentication isn’t set up yet.

## Testing

Run the unit tests from the workspace root:

```bash
python3 -m unittest discover skills/memory-keeper/tests
```

These tests cover copying defaults, logging entries, and gracefully handling git errors.

## Packaging & releases

When you’re ready to deliver a release, package the skill:

```bash
python3 $(npm root -g)/openclaw/skills/skill-creator/scripts/package_skill.py skills/memory-keeper
```

Share the resulting `memory-keeper.skill` file via ClawHub or GitHub releases.

## Links

- **GitHub:** https://github.com/CrimsonDevil333333/memory-keeper
- **ClawHub:** https://www.clawhub.ai/skills/memory-keeper


path: references/usage.md
content: # Memory Keeper usage reference

## Typical workflow

1. Update the workspace memory files (`memory/*.md`, `MEMORY.md`, and `AGENTS.md`, `SOUL.md`, `USER.md`, `TOOLS.md`, `HEARTBEAT.md`) as you go.
2. Run Memory Keeper to copy them to a dedicated archive directory or git repo, e.g.:  
   ```bash
   python3 skills/memory-keeper/scripts/memory_sync.py --target ~/clawdy-memories --commit --message "Post-session sync" --remote https://github.com/CrimsonDevil333333/clawdy-memories.git --push
   ```
3. The archive retains the same folder structure, so you can always pull it back into another workspace or inspect the history when you need context recovery.

## CLI options

- `--workspace / -w`: Path to your OpenClaw workspace (default: current directory).  
- `--target / -t`: Destination folder for the archive (default: `<workspace>/memory-archive`).  
- `--commit`: Run `git add .` and `git commit` after copying.  
- `--message`: Commit message (default: `Update memory archive`).  
- `--remote`: Optional git remote URL to configure before pushing.  
- `--branch`: Branch name for push (default: `master`).  
- `--push`: Push the commit after it is created (requires `--remote`).  
- `--allow-extra`: Additional workspace globs to include (e.g., `--allow-extra extra-notes.md`).  
- `--skip-memory`: Exclude `memory/` if you only want the high-level docs.

## Automation tips

- Install this skill on any agent that needs a resilient memory archive; just configure `--target` to a repo that you control.
- Run Memory Keeper before risky operations (system upgrades, config changes, restarts) so you can roll back with your last memory snapshot.
- Call `memory_sync.py` from a cron job or heartbeat check when you need regular backups, but add `--commit --message "Heartbeat sync"` and avoid `--push` unless you really need remote updates to prevent noise.

## Troubleshooting

- **`git` errors**: ensure the destination is writable and that you have `git` installed on your system.  
- **Remote refused or authentication failed**: Memory Keeper now surfaces the exact git command that failed and reminds you to configure your credential helper, SSH key, or embed a personal access token. Run `git remote -v` inside the archive to check the URL and try `GIT_ASKPASS=echo` + your token if needed.  
- **Files missing**: confirm you ran the command from the correct workspace or pass `--workspace` explicitly.

## Logging

Each successful sync appends a bullet to the current day’s `memory/YYYY-MM-DD.md` so you can trace when the memory archive last changed. If you ever need to audit a run, just read that entry for the snapshot path, commit/push flags, and remote URL used.


path: scripts/memory_sync.py
content: #!/usr/bin/env python3
"""Aligns workspace memory files into a persistent archive with optional git commits."""
from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Iterable, List, Optional, Sequence

DEFAULT_FILES = [
    "MEMORY.md",
    "AGENTS.md",
    "SOUL.md",
    "USER.md",
    "TOOLS.md",
    "HEARTBEAT.md",
]


def run_git_command(target: Path, args: List[str]) -> subprocess.CompletedProcess:
    return subprocess.run(["git", "-C", str(target)] + args, check=True, capture_output=True)


def copy_files(workspace: Path, dest: Path, include_memory: bool) -> None:
    if include_memory:
        memory_dir = workspace / "memory"
        if memory_dir.exists():
            for entry in memory_dir.glob("*.md"):
                relative = entry.relative_to(workspace)
                target_file = dest / relative
                target_file.parent.mkdir(parents=True, exist_ok=True)
                shutil.copy2(entry, target_file)
    for name in DEFAULT_FILES:
        source = workspace / name
        if source.exists():
            relative = source.relative_to(workspace)
            target_file = dest / relative
            target_file.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(source, target_file)


def ensure_git_repo(dest: Path) -> None:
    git_dir = dest / ".git"
    if not git_dir.exists():
        print("Initializing git repository at", dest)
        run_git_command(dest, ["init"])


def configure_remote(dest: Path, remote_url: str, remote_name: str = "origin") -> None:
    try:
        existing = run_git_command(dest, ["remote", "get-url", remote_name])
        current_url = existing.stdout.decode().strip()
    except subprocess.CalledProcessError:
        current_url = ""
    if current_url != remote_url:
        if current_url:
            run_git_command(dest, ["remote", "remove", remote_name])
        run_git_command(dest, ["remote", "add", remote_name, remote_url])


def handle_git_error(exc: subprocess.CalledProcessError, action: Sequence[str]) -> None:
    command = " ".join(str(part) for part in action)
    stderr = exc.stderr.decode().strip() if exc.stderr else ""
    stdout = exc.stdout.decode().strip() if exc.stdout else ""
    message = stderr or stdout or str(exc)
    print(f"Git command '{command}' failed: {message}", file=sys.stderr)
    print(
        "Hint: ensure the archive remote URL is accessible and any required tokens or SSH keys are configured.",
        file=sys.stderr,
    )


def log_memory_update(
    workspace: Path,
    target: Path,
    commit: bool,
    push: bool,
    remote: Optional[str],
    timestamp: Optional[datetime] = None,
) -> None:
    timestamp = timestamp or datetime.now().astimezone()
    memory_dir = workspace / "memory"
    memory_dir.mkdir(parents=True, exist_ok=True)
    entry = (
        f"- [{timestamp.strftime('%Y-%m-%d %H:%M:%S %Z')}] Memory Keeper synced to {target} "
        f"(commit={commit}, push={push}, remote={remote or 'none'})\n"
    )
    log_file = memory_dir / f"{timestamp.date():%Y-%m-%d}.md"
    with open(log_file, "a", encoding="utf-8") as handle:
        handle.write(entry)


def commit_and_push(target: Path, args: argparse.Namespace) -> bool:
    try:
        ensure_git_repo(target)
        run_git_command(target, ["add", "."])
        status = run_git_command(target, ["status", "--porcelain"]).stdout
        if status.strip():
            run_git_command(target, ["commit", "-m", args.message])
        if args.remote:
            configure_remote(target, args.remote)
        if args.push:
            git_args = ["push", "--set-upstream", "origin", args.branch]
            run_git_command(target, git_args)
    except subprocess.CalledProcessError as exc:
        handle_git_error(exc, exc.cmd if isinstance(exc.cmd, Sequence) else [str(exc.cmd)])
        return False
    return True


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Sync OpenClaw memory files into a durable archive."
    )
    parser.add_argument(
        "--workspace",
        "-w",
        default=".",
        help="Path to the OpenClaw workspace (defaults to current directory).",
    )
    parser.add_argument(
        "--target",
        "-t",
        default=None,
        help="Destination archive directory (defaults to ./memory-archive).",
    )
    parser.add_argument(
        "--commit",
        action="store_true",
        help="Commit the copied files when the target is a git repo.",
    )
    parser.add_argument(
        "--message",
        default="Update memory archive",
        help="Commit message to use."
    )
    parser.add_argument(
        "--remote",
        help="Remote URL to configure/push to (optional).",
    )
    parser.add_argument(
        "--branch",
        default="master",
        help="Branch to push to when --push is set (default: master).",
    )
    parser.add_argument(
        "--push",
        action="store_true",
        help="Push the commit to the remote after creating it (implies --remote).",
    )
    parser.add_argument(
        "--allow-extra",
        nargs="*",
        default=None,
        help="Additional relative files or glob patterns to include (space-separated).",
    )
    parser.add_argument(
        "--skip-memory",
        action="store_true",
        help="Skip copying the memory/ directory (only snapshot the core docs).",
    )

    args = parser.parse_args()

    workspace = Path(args.workspace).expanduser().resolve()
    target = (
        Path(args.target).expanduser().resolve()
        if args.target
        else (workspace / "memory-archive")
    )
    target.mkdir(parents=True, exist_ok=True)

    copy_files(workspace, target, include_memory=not args.skip_memory)

    if args.allow_extra:
        for pattern in args.allow_extra:
            matches = list(workspace.glob(pattern))
            for entry in matches:
                relative = entry.relative_to(workspace)
                target_file = target / relative
                target_file.parent.mkdir(parents=True, exist_ok=True)
                if entry.is_dir():
                    shutil.copytree(entry, target_file, dirs_exist_ok=True)
                else:
                    shutil.copy2(entry, target_file)

    if args.push and not args.remote:
        parser.error("--push requires --remote to be set")

    commit_result = True
    if args.commit:
        commit_result = commit_and_push(target, args)
    if not commit_result:
        return 1

    log_memory_update(workspace, target, args.commit, args.push, args.remote)

    print(f"Memory archive synchronized to {target}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: tests/test_memory_sync.py
content: import contextlib
import io
import importlib.util
import subprocess
import tempfile
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest.mock import patch

SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "memory_sync.py"
SPEC = importlib.util.spec_from_file_location("memory_sync", SCRIPT_PATH)
memory_sync = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(memory_sync)


class MemorySyncTests(unittest.TestCase):
    def test_copy_files_includes_defaults_and_memory(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            workspace = Path(tmpdir) / "workspace"
            workspace.mkdir()
            for name in memory_sync.DEFAULT_FILES:
                (workspace / name).write_text(f"content for {name}", encoding="utf-8")
            memory_dir = workspace / "memory"
            memory_dir.mkdir()
            (memory_dir / "session.md").write_text("session", encoding="utf-8")

            target = Path(tmpdir) / "archive"
            memory_sync.copy_files(workspace, target, include_memory=True)

            for name in memory_sync.DEFAULT_FILES:
                self.assertTrue((target / name).exists(), f"{name} was not copied")
            self.assertTrue((target / "memory" / "session.md").exists())

    def test_log_memory_update_writes_entry_with_timestamp(self):
        timestamp = datetime(2026, 2, 3, 12, 0)
        with tempfile.TemporaryDirectory() as tmpdir:
            workspace = Path(tmpdir)
            target = Path(tmpdir) / "dest"
            target.mkdir()

            memory_sync.log_memory_update(
                workspace,
                target,
                commit=True,
                push=False,
                remote="https://example.com/repo.git",
                timestamp=timestamp,
            )

            log_file = workspace / "memory" / "2026-02-03.md"
            self.assertTrue(log_file.exists())
            contents = log_file.read_text(encoding="utf-8")
            self.assertIn("Memory Keeper synced to", contents)
            self.assertIn("commit=True", contents)

    def test_commit_and_push_handles_git_errors(self):
        args = SimpleNamespace(
            message="msg",
            remote="https://example.com/repo.git",
            push=True,
            branch="main",
            commit=True,
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            target = Path(tmpdir) / "archive"
            target.mkdir()
            error = subprocess.CalledProcessError(
                returncode=1,
                cmd=["git", "-C", str(target), "add", "."],
                stderr=b"fatal: Authentication failed",
            )

            with patch.object(memory_sync, "ensure_git_repo"), \
                    patch.object(memory_sync, "configure_remote"), \
                    patch.object(memory_sync, "run_git_command", side_effect=error):
                buffer = io.StringIO()
                with contextlib.redirect_stderr(buffer):
                    result = memory_sync.commit_and_push(target, args)
                self.assertFalse(result)
                self.assertIn("Git command", buffer.getvalue())
                self.assertIn("Authentication failed", buffer.getvalue())


if __name__ == "__main__":
    unittest.main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)