Skill: yepthatsjason/memstate-ai (version 1.2.4)

--- name: memstate-ai description: > Versioned, structured memory for AI agents. Every fact is tracked, every change is logged, and your agent always gets the current answer — not a pile of outdated context. Requires MEMSTATE_API_KEY. metadata: {"openclaw": {"requires": {"bins": ["python3"], "env": ["MEMSTATE_API_KEY"]}, "primaryEnv": "MEMSTATE_API_KEY", "homepage": "https://memstate.ai"}} --- # Memstate AI Memory Management This skill provides a full-featured alternative to the Memstate MCP plugin by interacting directly with the Memstate REST API. It gives agents a persistent, structured, and versioned knowledge base with conflict detection, semantic search, and full version history. ## Core Concepts | Concept | Description | |---|---| | **Project** | Top-level container for memories (e.g., `myapp`, `backend-api`). Auto-created on first write. | | **Keypath** | Dot-separated hierarchical path (e.g., `auth.method`). Auto-prefixed with `project.{project_id}.` | | **Memory** | A single fact or markdown summary stored at a keypath with full version history. | | **Versioning** | Writing to an existing keypath supersedes the old value. History is always preserved. | | **Tombstone** | Deleting a keypath creates a tombstone version — history is never destroyed. | ## Input Formats ### Direct keypath = value assignment ``` config.port = 8080 database.engine = PostgreSQL 16 auth.method = JWT with httpOnly cookies status.deployment = production ``` ### Markdown (preferred for task summaries) ```markdown ## Architecture Decision - Database: PostgreSQL 16 - Auth: JWT with httpOnly cookies - Deploy: Docker on AWS ECS - API style: REST with OpenAPI 3.1 ``` ## Workflows ### Before Starting a Task (Recall) Always check what already exists before making decisions or modifying code. ```bash # 1. Semantic search — find relevant facts by meaning python3 {baseDir}/scripts/memstate_search.py \ --project "myapp" \ --query "how is authentication configured" # 2. Browse the full project tree (all domains and keypaths) python3 {baseDir}/scripts/memstate_get.py \ --project "myapp" # 3. Get a specific subtree with full content python3 {baseDir}/scripts/memstate_get.py \ --project "myapp" --keypath "database" --include-content ``` ### After Completing a Task (Remember) ```bash # Store a single fact (config, status, version numbers) python3 {baseDir}/scripts/memstate_set.py \ --project "myapp" \ --keypath "config.port" \ --value "8080" \ --category "fact" # Store a rich markdown summary (AI extracts keypaths automatically) python3 {baseDir}/scripts/memstate_remember.py \ --project "myapp" \ --content "## Auth Migration\n- Changed from JWT to server-side sessions\n- Added MFA via TOTP\n- Files: auth.go, middleware.go" \ --source "agent" ``` ### Manage History and Cleanup ```bash # View how a fact changed over time python3 {baseDir}/scripts/memstate_history.py \ --project "myapp" --keypath "database.engine" # Soft-delete an outdated keypath (history preserved) python3 {baseDir}/scripts/memstate_delete.py \ --project "myapp" --keypath "config.old_setting" # Soft-delete an entire project python3 {baseDir}/scripts/memstate_delete_project.py \ --project "myapp" ``` ## Script Reference ### `memstate_set.py` — Set a single keypath value Stores one fact at a specific keypath. Synchronous, immediately available. Supersedes the previous value if the keypath already exists. ```bash python3 {baseDir}/scripts/memstate_set.py \ --project PROJECT_ID \ --keypath KEYPATH \ --value VALUE \ [--category CATEGORY] # decision | fact | config | requirement | note | code | learning [--topics TAG1,TAG2] ``` **Response keys:** `action` (created|superseded), `memory_id`, `version` --- ### `memstate_remember.py` — Ingest markdown or text Preferred for task summaries, meeting notes, or any multi-fact content. The AI engine automatically extracts structured keypaths from your text. Processing is async (~15–18 s); the script polls until completion. ```bash python3 {baseDir}/scripts/memstate_remember.py \ --project PROJECT_ID \ --content "MARKDOWN_OR_TEXT" \ [--source agent|readme|docs|meeting|code] \ [--context "optional hint for extraction"] ``` **Response keys:** `status` (completed|failed), `job_id`, `memories_created` --- ### `memstate_get.py` — Browse and retrieve memories ```bash # List all projects python3 {baseDir}/scripts/memstate_get.py # Full project tree (returns domains and keypaths) python3 {baseDir}/scripts/memstate_get.py --project PROJECT_ID # Subtree at a keypath python3 {baseDir}/scripts/memstate_get.py \ --project PROJECT_ID --keypath KEYPATH [--include-content] [--at-revision N] # Single memory by UUID python3 {baseDir}/scripts/memstate_get.py --memory-id UUID ``` **Response keys (project tree):** `domains`, `total_memories` **Response keys (subtree):** `memories`, `total_count` **Response keys (list projects):** `projects` --- ### `memstate_search.py` — Semantic search Find memories by meaning when you don't know the exact keypath. ```bash python3 {baseDir}/scripts/memstate_search.py \ --query "NATURAL_LANGUAGE_QUERY" \ [--project PROJECT_ID] \ [--limit N] # default 20, max 100 ``` **Response keys:** `results` (array), `total_found`, `query` --- ### `memstate_history.py` — Version history View all versions of a keypath or memory chain. ```bash python3 {baseDir}/scripts/memstate_history.py \ --project PROJECT_ID --keypath KEYPATH # or python3 {baseDir}/scripts/memstate_history.py \ --memory-id UUID ``` **Response keys:** `versions` (array), `total_versions` --- ### `memstate_delete.py` — Soft-delete a keypath Creates a tombstone version. History is always preserved. ```bash python3 {baseDir}/scripts/memstate_delete.py \ --project PROJECT_ID \ --keypath KEYPATH \ [--recursive] # delete entire subtree ``` **Response keys:** `deleted_count`, `deleted_keypaths` --- ### `memstate_delete_project.py` — Soft-delete a project ```bash python3 {baseDir}/scripts/memstate_delete_project.py \ --project PROJECT_ID ``` **Response keys:** `project_id`, `deleted_count` --- ## Best Practices 1. **One keypath = one fact.** Use `api.style` not `api`. Be specific. 2. **Update, don't duplicate.** When a fact changes, call `memstate_set.py` with the SAME keypath and the NEW value. Do not create a new keypath. 3. **Trust `is_latest: true`.** Search results may show multiple versions. Only trust results where `is_latest` is `true`. 4. **Use Markdown for summaries.** `memstate_remember.py` excels at parsing Markdown lists, headings, and key-value pairs into structured keypaths. 5. **Search before browsing.** `memstate_search.py` is faster than browsing the tree when you know what you're looking for. 6. **Use categories.** Setting `--category decision` on architecture choices makes them easier to filter later. ## Authentication Set the `MEMSTATE_API_KEY` environment variable before running any script: ```bash export MEMSTATE_API_KEY="your_api_key_here" ``` Get your API key at [https://memstate.ai/dashboard](https://memstate.ai/dashboard). ## Resources - **Documentation:** [https://memstate.ai/docs](https://memstate.ai/docs) - **MCP Plugin (alternative):** [https://github.com/memstate-ai/memstate-mcp](https://github.com/memstate-ai/memstate-mcp) - **Benchmark:** [https://github.com/memstate-ai/memstate-benchmark](https://github.com/memstate-ai/memstate-benchmark)

path: scripts/memstate_delete.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
import urllib.request

[REDACTED_SECRET]"MEMSTATE_API_KEY")
if not [REDACTED_SECRET]"Error: MEMSTATE_API_KEY environment variable is required", file=sys.stderr)
    sys.exit(1)
BASE_URL = "https://api.memstate.ai/api/v1"

def delete_memory(project_id, keypath, recursive=False):
    url = f"{BASE_URL}/memories/delete"
    headers = {
        "X-API-Key": API_KEY,
        "Content-Type": "application/json",
        "User-Agent": "memstate-skill/1.0"
    }
    
    data = {
        "project_id": project_id,
        "keypath": keypath,
        "recursive": recursive
    }

    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
    
    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode("utf-8"))
            print(json.dumps(result, indent=2))
            return 0
    except urllib.error.HTTPError as e:
        print(f"Error: {e.code} - {e.read().decode('utf-8')}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Soft-delete a memory by keypath")
    parser.add_argument("--project", required=True, help="Project ID")
    parser.add_argument("--keypath", required=True, help="Keypath to delete")
    parser.add_argument("--recursive", action="store_true", help="Delete the entire keypath subtree")
    
    args = parser.parse_args()
    sys.exit(delete_memory(args.project, args.keypath, args.recursive))


path: scripts/memstate_delete_project.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
import urllib.request

[REDACTED_SECRET]"MEMSTATE_API_KEY")
if not [REDACTED_SECRET]"Error: MEMSTATE_API_KEY environment variable is required", file=sys.stderr)
    sys.exit(1)
BASE_URL = "https://api.memstate.ai/api/v1"

def delete_project(project_id):
    url = f"{BASE_URL}/projects/delete"
    headers = {
        "X-API-Key": API_KEY,
        "Content-Type": "application/json",
        "User-Agent": "memstate-skill/1.0"
    }
    
    data = {
        "project_id": project_id
    }

    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
    
    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode("utf-8"))
            print(json.dumps(result, indent=2))
            return 0
    except urllib.error.HTTPError as e:
        print(f"Error: {e.code} - {e.read().decode('utf-8')}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Soft-delete an entire project")
    parser.add_argument("--project", required=True, help="Project ID to delete")
    
    args = parser.parse_args()
    sys.exit(delete_project(args.project))


path: scripts/memstate_get.py
content: #!/usr/bin/env python3
"""
memstate_get.py — Browse and retrieve memories from Memstate AI.

Usage:
  python3 memstate_get.py                                    # List all projects
  python3 memstate_get.py --project myapp                   # Full project tree (domains)
  python3 memstate_get.py --project myapp --keypath db      # Subtree at keypath
  python3 memstate_get.py --project myapp --keypath db --include-content
  python3 memstate_get.py --memory-id <uuid>                # Single memory by ID
  python3 memstate_get.py --project myapp --keypath db --at-revision 3
"""
import argparse
import json
import os
import sys
import urllib.request

[REDACTED_SECRET]"MEMSTATE_API_KEY")
if not [REDACTED_SECRET]"Error: MEMSTATE_API_KEY environment variable is required", file=sys.stderr)
    sys.exit(1)
BASE_URL = "https://api.memstate.ai/api/v1"

HEADERS = {
    "X-API-Key": API_KEY,
    "Content-Type": "application/json",
    "User-Agent": "memstate-skill/1.0"
}


def get_memories(project_id=None, keypath=None, memory_id=None, include_content=False, at_revision=None):
    try:
        if memory_id:
            # GET /memories/{id} — single memory by UUID
            url = f"{BASE_URL}/memories/{memory_id}"
            req = urllib.request.Request(url, headers=HEADERS)

        elif project_id and keypath:
            # POST /keypaths — subtree at a specific keypath
            url = f"{BASE_URL}/keypaths"
            data = {
                "project_id": project_id,
                "keypath": keypath,
                "recursive": True,
                "include_content": include_content,
            }
            if at_revision:
                data["at_revision"] = at_revision
            req = urllib.request.Request(
                url, data=json.dumps(data).encode("utf-8"), headers=HEADERS, method="POST"
            )

        elif project_id:
            # GET /tree — full project domain tree
            url = f"{BASE_URL}/tree?project_id={project_id}"
            req = urllib.request.Request(url, headers=HEADERS)

        else:
            # GET /projects — list all projects
            print("Listing all projects...", file=sys.stderr)
            url = f"{BASE_URL}/projects"
            req = urllib.request.Request(url, headers=HEADERS)

        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode("utf-8"))
            print(json.dumps(result, indent=2))
            return 0

    except urllib.error.HTTPError as e:
        print(f"Error: {e.code} - {e.read().decode('utf-8')}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Browse and retrieve memories from Memstate AI",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument("--project", help="Project ID")
    parser.add_argument("--keypath", help="Subkeypath within project (e.g. 'database' or 'config.port')")
    parser.add_argument("--memory-id", help="Get a single memory by its UUID")
    parser.add_argument("--include-content", action="store_true", help="Include full memory content in response")
    parser.add_argument("--at-revision", type=int, help="Optional revision number for time-travel queries")

    args = parser.parse_args()
    sys.exit(get_memories(args.project, args.keypath, args.memory_id, args.include_content, args.at_revision))


path: scripts/memstate_history.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
import urllib.request

[REDACTED_SECRET]"MEMSTATE_API_KEY")
if not [REDACTED_SECRET]"Error: MEMSTATE_API_KEY environment variable is required", file=sys.stderr)
    sys.exit(1)
BASE_URL = "https://api.memstate.ai/api/v1"

def get_history(project_id=None, keypath=None, memory_id=None):
    url = f"{BASE_URL}/memories/history"
    headers = {
        "X-API-Key": API_KEY,
        "Content-Type": "application/json",
        "User-Agent": "memstate-skill/1.0"
    }
    
    data = {}
    if memory_id:
        data["memory_id"] = memory_id
    elif project_id and keypath:
        data["project_id"] = project_id
        data["keypath"] = keypath
    else:
        print("Error: Must provide either --memory-id OR both --project and --keypath", file=sys.stderr)
        return 1

    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
    
    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode("utf-8"))
            print(json.dumps(result, indent=2))
            return 0
    except urllib.error.HTTPError as e:
        print(f"Error: {e.code} - {e.read().decode('utf-8')}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="View version history for a keypath or memory chain")
    parser.add_argument("--project", help="Project ID (required with --keypath)")
    parser.add_argument("--keypath", help="Keypath to get history for")
    parser.add_argument("--memory-id", help="Memory ID to get history for")
    
    args = parser.parse_args()
    sys.exit(get_history(args.project, args.keypath, args.memory_id))


path: scripts/memstate_remember.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
import time
import urllib.request

[REDACTED_SECRET]"MEMSTATE_API_KEY")
if not [REDACTED_SECRET]"Error: MEMSTATE_API_KEY environment variable is required", file=sys.stderr)
    sys.exit(1)
BASE_URL = "https://api.memstate.ai/api/v1"

def remember_content(project_id, content, source=None, context=None):
    url = f"{BASE_URL}/ingest"
    headers = {
        "X-API-Key": API_KEY,
        "Content-Type": "application/json",
        "User-Agent": "memstate-skill/1.0"
    }
    
    data = {
        "project_id": project_id,
        "content": content
    }
    
    if source:
        data["source"] = source
    if context:
        data["context"] = context

    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
    
    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode("utf-8"))
            job_id = result.get("job_id")
            if not job_id:
                print(json.dumps(result, indent=2))
                return 0
                
            print(f"Job started with ID: {job_id}. Polling for completion...")
            
            # Poll for completion
            while True:
                time.sleep(2)
                status_url = f"{BASE_URL}/jobs/{job_id}"
                status_req = urllib.request.Request(status_url, headers=headers)
                
                with urllib.request.urlopen(status_req) as status_resp:
                    status_result = json.loads(status_resp.read().decode("utf-8"))
                    status = status_result.get("status")
                    
                    if status in ["completed", "failed", "error"]:
                        print(json.dumps(status_result, indent=2))
                        return 0 if status == "completed" else 1
                        
    except urllib.error.HTTPError as e:
        print(f"Error: {e.code} - {e.read().decode('utf-8')}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Save markdown or text, extracting keypaths automatically")
    parser.add_argument("--project", required=True, help="Project ID")
    parser.add_argument("--content", required=True, help="Markdown or text to remember")
    parser.add_argument("--source", help="Source type (agent, readme, docs, etc.)")
    parser.add_argument("--context", help="Optional hint to guide extraction")
    
    args = parser.parse_args()
    sys.exit(remember_content(args.project, args.content, args.source, args.context))


path: scripts/memstate_search.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
import urllib.request

[REDACTED_SECRET]"MEMSTATE_API_KEY")
if not [REDACTED_SECRET]"Error: MEMSTATE_API_KEY environment variable is required", file=sys.stderr)
    sys.exit(1)
BASE_URL = "https://api.memstate.ai/api/v1"

def search_memories(query, project_id=None, limit=20):
    url = f"{BASE_URL}/memories/search"
    headers = {
        "X-API-Key": API_KEY,
        "Content-Type": "application/json",
        "User-Agent": "memstate-skill/1.0"
    }
    
    data = {
        "query": query,
        "limit": limit
    }
    
    if project_id:
        data["project_id"] = project_id

    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
    
    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode("utf-8"))
            print(json.dumps(result, indent=2))
            return 0
    except urllib.error.HTTPError as e:
        print(f"Error: {e.code} - {e.read().decode('utf-8')}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Semantic search by meaning")
    parser.add_argument("--query", required=True, help="Natural language search query")
    parser.add_argument("--project", help="Filter by project ID")
    parser.add_argument("--limit", type=int, default=20, help="Maximum results (default: 20)")
    
    args = parser.parse_args()
    sys.exit(search_memories(args.query, args.project, args.limit))


path: scripts/memstate_set.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
import urllib.request

[REDACTED_SECRET]"MEMSTATE_API_KEY")
if not [REDACTED_SECRET]"Error: MEMSTATE_API_KEY environment variable is required", file=sys.stderr)
    sys.exit(1)
BASE_URL = "https://api.memstate.ai/api/v1"

def set_memory(project_id, keypath, value, category=None, topics=None):
    url = f"{BASE_URL}/memories/remember"
    headers = {
        "X-API-Key": API_KEY,
        "Content-Type": "application/json",
        "User-Agent": "memstate-skill/1.0"
    }
    
    data = {
        "project_id": project_id,
        "keypath": keypath,
        "content": value
    }
    
    if category:
        data["category"] = category
    if topics:
        data["topics"] = topics.split(",")

    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), headers=headers, method="POST")
    
    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode("utf-8"))
            print(json.dumps(result, indent=2))
            return 0
    except urllib.error.HTTPError as e:
        print(f"Error: {e.code} - {e.read().decode('utf-8')}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Set a single fact at a specific keypath")
    parser.add_argument("--project", required=True, help="Project ID")
    parser.add_argument("--keypath", required=True, help="Hierarchical path (e.g., config.port)")
    parser.add_argument("--value", required=True, help="Value to store")
    parser.add_argument("--category", help="Category (decision, fact, config, etc.)")
    parser.add_argument("--topics", help="Comma-separated list of topics")
    
    args = parser.parse_args()
    sys.exit(set_memory(args.project, args.keypath, args.value, args.category, args.topics))


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