diff --git a/docs/docs.json b/docs/docs.json
index a966380a..e93cfff1 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -184,6 +184,14 @@
}
]
},
+ {
+ "tab": "OpenMemory",
+ "icon": "square-terminal",
+ "pages": [
+ "openmemory/overview",
+ "openmemory/quickstart"
+ ]
+ },
{
"tab": "Examples",
"groups": [
diff --git a/docs/openmemory/overview.mdx b/docs/openmemory/overview.mdx
new file mode 100644
index 00000000..ff46d9d7
--- /dev/null
+++ b/docs/openmemory/overview.mdx
@@ -0,0 +1,81 @@
+---
+title: Overview
+icon: "info"
+iconType: "solid"
+---
+
+
+
+OpenMemory is a local memory infrastructure powered by Mem0 that lets you carry your memory accross any AI app. It provides a unified memory layer that stays with you, enabling agents and assistants to remember what matters across applications.
+
+
+
+## What is the OpenMemory MCP Server
+
+The OpenMemory MCP Server is a private, local-first memory server that creates a shared, persistent memory layer for your MCP-compatible tools. This runs entirely on your machine, enabling seamless context handoff across tools. Whether you're switching between development, planning, or debugging environments, your AI assistants can access relevant memory without needing repeated instructions.
+
+The OpenMemory MCP Server ensures all memory stays local, structured, and under your control with no cloud sync or external storage.
+
+## How the OpenMemory MCP Server Works
+
+Built around the Model Context Protocol (MCP), the OpenMemory MCP Server exposes a standardized set of memory tools:
+- `add_memories`: Store new memory objects
+- `search_memory`: Retrieve relevant memories
+- `list_memories`: View all stored memory
+- `delete_all_memories`: Clear memory entirely
+
+Any MCP-compatible tool can connect to the server and use these APIs to persist and access memory.
+
+## What It Enables
+
+### Cross-Client Memory Access
+Store context in Cursor and retrieve it later in Claude or Windsurf without repeating yourself.
+
+### Fully Local Memory Store
+All memory is stored on your machine. Nothing goes to the cloud. You maintain full ownership and control.
+
+### Unified Memory UI
+The built-in OpenMemory dashboard provides a central view of everything stored. Add, browse, delete and control memory access to clients directly from the dashboard.
+
+## Supported Clients
+
+The OpenMemory MCP Server is compatible with any client that supports the Model Context Protocol. This includes:
+- Cursor
+- Claude Desktop
+- Windsurf
+- Cline, and more.
+
+As more AI systems adopt MCP, your private memory becomes more valuable.
+
+## Real-World Examples
+
+### Scenario 1: Cross-Tool Project Flow
+Define technical requirements of a project in Claude Desktop. Build in Cursor. Debug issues in Windsurf - all with shared context passed through OpenMemory.
+
+### Scenario 2: Preferences That Persist
+Set your preferred code style or tone in one tool. When you switch to another MCP client, it can access those same preferences without redefining them.
+
+### Scenario 3: Project Knowledge
+Save important project details once, then access them from any compatible AI tool, no more repetitive explanations.
+
+## Conclusion
+
+The OpenMemory MCP Server brings memory to MCP-compatible tools without giving up control or privacy. It solves a foundational limitation in modern LLM workflows: the loss of context across tools, sessions, and environments.
+
+By standardizing memory operations and keeping all data local, it reduces token overhead, improves performance, and unlocks more intelligent interactions across the growing ecosystem of AI assistants.
+
+This is just the beginning. The MCP server is the first core layer in the OpenMemory platform - a broader effort to make memory portable, private, and interoperable across AI systems.
+
+## Getting Started Today
+
+- Github Repository: https://github.com/mem0ai/mem0
+- Read the documentation: [Docs Link]
+- Join our community: [Discord link]
+
+With OpenMemory, your AI memories stay private, portable, and under your control, exactly where they belong.
+
+OpenMemory: Your memories, your control.
+
+## Contributing
+
+OpenMemory is open source and we welcome contributions. Please see the [CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/openmemory/CONTRIBUTING.md) file for more information.
\ No newline at end of file
diff --git a/docs/openmemory/quickstart.mdx b/docs/openmemory/quickstart.mdx
new file mode 100644
index 00000000..0750ca85
--- /dev/null
+++ b/docs/openmemory/quickstart.mdx
@@ -0,0 +1,47 @@
+---
+title: Quickstart
+icon: "terminal"
+iconType: "solid"
+---
+
+
+
+
+## Setting Up OpenMemory
+
+Getting started with OpenMemory is straight forward and takes just a few minutes to set up on your local machine. Follow these steps:
+
+### Getting started
+First clone the repository and then follow the instructions:
+```bash
+# Clone the repository
+git clone https://github.com/mem0ai/mem0.git
+cd mem0/openmemory
+
+# Create the backend .env file with your OpenAI key
+pushd api && echo "OPENAI_API_KEY=your_key_here" > .env && popd
+
+# Build the Docker images
+make build
+
+# Start all services (API server, vector database, and MCP server components)
+make up
+
+# Start the frontend
+cp ui/.env.example ui/.env
+make ui
+```
+
+You can configure the MCP client using the following command (replace username with your username):
+
+```bash
+npx install-mcp i "http://localhost:8765/mcp/cursor/sse/username" --client cursor
+```
+
+The OpenMemory dashboard will be available at http://localhost:3000. From here, you can view and manage your memories, as well as check connection status with your MCP clients.
+
+Once set up, OpenMemory runs locally on your machine, ensuring all your AI memories remain private and secure while being accessible across any compatible MCP client.
+
+### Getting Started Today
+
+- Github Repository: https://github.com/mem0ai/mem0/openmemory
\ No newline at end of file
diff --git a/openmemory/.gitignore b/openmemory/.gitignore
new file mode 100644
index 00000000..4337750e
--- /dev/null
+++ b/openmemory/.gitignore
@@ -0,0 +1,13 @@
+*.db
+.env*
+!.env.example
+!.env.dev
+!ui/lib
+.venv/
+__pycache__
+.DS_Store
+node_modules/
+*.log
+api/.openmemory*
+**/.next
+.openmemory/
\ No newline at end of file
diff --git a/openmemory/CONTRIBUTING.md b/openmemory/CONTRIBUTING.md
new file mode 100644
index 00000000..701334d4
--- /dev/null
+++ b/openmemory/CONTRIBUTING.md
@@ -0,0 +1,70 @@
+# Contributing to OpenMemory
+
+We are a team of developers passionate about the future of AI and open-source software. With years of experience in both fields, we believe in the power of community-driven development and are excited to build tools that make AI more accessible and personalized.
+
+## Ways to Contribute
+
+We welcome all forms of contributions:
+- Bug reports and feature requests through GitHub Issues
+- Documentation improvements
+- Code contributions
+- Testing and feedback
+- Community support and discussions
+
+## Development Workflow
+
+1. Fork the repository
+2. Create your feature branch (`git checkout -b openmemory/feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+4. Push to the branch (`git push origin openmemory/feature/amazing-feature`)
+5. Open a Pull Request
+
+## Development Setup
+
+### Backend Setup
+
+```bash
+# Copy environment file and edit file to update OPENAI_API_KEY and other secrets
+make env
+
+# Build the containers
+make build
+
+# Start the services
+make up
+```
+
+### Frontend Setup
+
+The frontend is a React application. To start the frontend:
+
+```bash
+# Install dependencies and start the development server
+make ui-dev
+```
+
+### Prerequisites
+- Docker and Docker Compose
+- Python 3.9+ (for backend development)
+- Node.js (for frontend development)
+- OpenAI API Key (for LLM interactions)
+
+### Getting Started
+Follow the setup instructions in the README.md file to set up your development environment.
+
+## Code Standards
+
+We value:
+- Clean, well-documented code
+- Thoughtful discussions about features and improvements
+- Respectful and constructive feedback
+- A welcoming environment for all contributors
+
+## Pull Request Process
+
+1. Ensure your code follows the project's coding standards
+2. Update documentation as needed
+3. Include tests for new features
+4. Make sure all tests pass before submitting
+
+Join us in building the future of AI memory management! Your contributions help make OpenMemory better for everyone.
diff --git a/openmemory/Makefile b/openmemory/Makefile
new file mode 100644
index 00000000..8e6344bf
--- /dev/null
+++ b/openmemory/Makefile
@@ -0,0 +1,70 @@
+.PHONY: help up down logs shell migrate test test-clean env ui-install ui-start ui-dev
+
+NEXT_PUBLIC_USER_ID=$(USER)
+NEXT_PUBLIC_API_URL=http://localhost:8765
+
+# Default target
+help:
+ @echo "Available commands:"
+ @echo " make env - Copy .env.example to .env"
+ @echo " make up - Start the containers"
+ @echo " make down - Stop the containers"
+ @echo " make logs - Show container logs"
+ @echo " make shell - Open a shell in the api container"
+ @echo " make migrate - Run database migrations"
+ @echo " make test - Run tests in a new container"
+ @echo " make test-clean - Run tests and clean up volumes"
+ @echo " make ui-install - Install frontend dependencies"
+ @echo " make ui-start - Start the frontend development server"
+ @echo " make ui - Install dependencies and start the frontend"
+
+env:
+ cd api && cp .env.example .env
+
+build:
+ cd api && docker-compose build
+
+up:
+ cd api && docker-compose up
+
+down:
+ cd api && docker-compose down -v
+ rm -f api/openmemory.db
+
+logs:
+ cd api && docker-compose logs -f
+
+shell:
+ cd api && docker-compose exec api bash
+
+upgrade:
+ cd api && docker-compose exec api alembic upgrade head
+
+migrate:
+ cd api && docker-compose exec api alembic upgrade head
+
+downgrade:
+ cd api && docker-compose exec api alembic downgrade -1
+
+test:
+ cd api && docker-compose run --rm api pytest tests/ -v
+
+test-clean:
+ cd api && docker-compose run --rm api pytest tests/ -v && docker-compose down -v
+
+# Frontend commands
+ui-install:
+ cd ui && pnpm install
+
+ui-build:
+ cd ui && pnpm build
+
+ui-start:
+ cd ui && NEXT_PUBLIC_USER_ID=$(USER) NEXT_PUBLIC_API_URL=$(NEXT_PUBLIC_API_URL) pnpm start
+
+ui-dev-start:
+ cd ui && NEXT_PUBLIC_USER_ID=$(USER) NEXT_PUBLIC_API_URL=$(NEXT_PUBLIC_API_URL) && pnpm dev
+
+ui-dev: ui-install ui-dev-start
+
+ui: ui-install ui-build ui-start
\ No newline at end of file
diff --git a/openmemory/README.md b/openmemory/README.md
new file mode 100644
index 00000000..d7f54017
--- /dev/null
+++ b/openmemory/README.md
@@ -0,0 +1,51 @@
+# OpenMemory
+
+OpenMemory is your personal memory layer for LLMs - private, portable, and open-source. Your memories live locally, giving you complete control over your data. Build AI applications with personalized memories while keeping your data secure.
+
+
+
+## Prerequisites
+
+- Docker and Docker Compose
+- Python 3.9+ (for backend development)
+- Node.js (for frontend development)
+- OpenAI API Key (required for LLM interactions)
+
+## Quickstart
+
+You can run the project using the following two commands:
+```bash
+make build # builds the mcp server
+make up # runs openmemory mcp server
+make ui # runs openmemory ui
+```
+
+After running these commands, you will have:
+- OpenMemory MCP server running at: http://localhost:8765 (API documentation available at http://localhost:8765/docs)
+- OpenMemory UI running at: http://localhost:3000
+
+## Project Structure
+
+- `api/` - Backend APIs + MCP server
+- `ui/` - Frontend React application
+
+## Contributing
+
+We are a team of developers passionate about the future of AI and open-source software. With years of experience in both fields, we believe in the power of community-driven development and are excited to build tools that make AI more accessible and personalized.
+
+We welcome all forms of contributions:
+- Bug reports and feature requests
+- Documentation improvements
+- Code contributions
+- Testing and feedback
+- Community support
+
+How to contribute:
+
+1. Fork the repository
+2. Create your feature branch (`git checkout -b openmemory/feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+4. Push to the branch (`git push origin openmemory/feature/amazing-feature`)
+5. Open a Pull Request
+
+Join us in building the future of AI memory management! Your contributions help make OpenMemory better for everyone.
diff --git a/openmemory/api/.env.example b/openmemory/api/.env.example
new file mode 100644
index 00000000..2627a0e9
--- /dev/null
+++ b/openmemory/api/.env.example
@@ -0,0 +1,2 @@
+OPENAI_API_KEY=sk-...
+USER=username
\ No newline at end of file
diff --git a/openmemory/api/.python-version b/openmemory/api/.python-version
new file mode 100644
index 00000000..fdcfcfdf
--- /dev/null
+++ b/openmemory/api/.python-version
@@ -0,0 +1 @@
+3.12
\ No newline at end of file
diff --git a/openmemory/api/Dockerfile b/openmemory/api/Dockerfile
new file mode 100644
index 00000000..5dd9fece
--- /dev/null
+++ b/openmemory/api/Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.12-slim
+
+LABEL org.opencontainers.image.name="mem0/openmemory-mcp"
+
+WORKDIR /usr/src/openmemory
+
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+
+COPY . .
+
+EXPOSE 8765
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8765"]
diff --git a/openmemory/api/README.md b/openmemory/api/README.md
new file mode 100644
index 00000000..3e92170f
--- /dev/null
+++ b/openmemory/api/README.md
@@ -0,0 +1,53 @@
+# OpenMemory API
+
+This directory contains the backend API for OpenMemory, built with FastAPI and SQLAlchemy. This also runs the Mem0 MCP Server that you can use with MCP clients to remember things.
+
+## Quick Start with Docker (Recommended)
+
+The easiest way to get started is using Docker. Make sure you have Docker and Docker Compose installed.
+
+1. Build the containers:
+```bash
+make build
+```
+
+2. Start the services:
+```bash
+make up
+```
+
+The API will be available at `http://localhost:8765`
+
+### Common Docker Commands
+
+- View logs: `make logs`
+- Open shell in container: `make shell`
+- Run database migrations: `make migrate`
+- Run tests: `make test`
+- Run tests and clean up: `make test-clean`
+- Stop containers: `make down`
+
+## API Documentation
+
+Once the server is running, you can access the API documentation at:
+- Swagger UI: `http://localhost:8765/docs`
+- ReDoc: `http://localhost:8765/redoc`
+
+## Project Structure
+
+- `app/`: Main application code
+ - `models.py`: Database models
+ - `database.py`: Database configuration
+ - `routers/`: API route handlers
+- `migrations/`: Database migration files
+- `tests/`: Test files
+- `alembic/`: Alembic migration configuration
+- `main.py`: Application entry point
+
+## Development Guidelines
+
+- Follow PEP 8 style guide
+- Use type hints
+- Write tests for new features
+- Update documentation when making changes
+- Run migrations for database changes
diff --git a/openmemory/api/alembic.ini b/openmemory/api/alembic.ini
new file mode 100644
index 00000000..8cddf4fe
--- /dev/null
+++ b/openmemory/api/alembic.ini
@@ -0,0 +1,114 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+# Use forward slashes (/) also on windows to provide an os agnostic path
+script_location = alembic
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
+# for all available tokens
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python-dateutil library that can be
+# installed by adding `alembic[tz]` to the pip requirements
+# timezone =
+
+# max length of characters to apply to the "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to alembic/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or colons.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+sqlalchemy.url = sqlite:///./openmemory.db
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
+# hooks = ruff
+# ruff.type = exec
+# ruff.executable = %(here)s/.venv/bin/ruff
+# ruff.options = check --fix REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/openmemory/api/alembic/README b/openmemory/api/alembic/README
new file mode 100644
index 00000000..98e4f9c4
--- /dev/null
+++ b/openmemory/api/alembic/README
@@ -0,0 +1 @@
+Generic single-database configuration.
\ No newline at end of file
diff --git a/openmemory/api/alembic/env.py b/openmemory/api/alembic/env.py
new file mode 100644
index 00000000..b4295b69
--- /dev/null
+++ b/openmemory/api/alembic/env.py
@@ -0,0 +1,92 @@
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+import os
+import sys
+from dotenv import load_dotenv
+
+# Add the parent directory to the Python path
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# Load environment variables
+load_dotenv()
+
+# Import your models here
+from app.database import Base
+from app.models import * # Import all your models
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+target_metadata = Base.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = os.getenv("DATABASE_URL", "sqlite:///./openmemory.db")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ configuration = config.get_section(config.config_ini_section)
+ configuration["sqlalchemy.url"] = os.getenv("DATABASE_URL", "sqlite:///./openmemory.db")
+ connectable = engine_from_config(
+ configuration,
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection, target_metadata=target_metadata
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/openmemory/api/alembic/script.py.mako b/openmemory/api/alembic/script.py.mako
new file mode 100644
index 00000000..480b130d
--- /dev/null
+++ b/openmemory/api/alembic/script.py.mako
@@ -0,0 +1,28 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision: str = ${repr(up_revision)}
+down_revision: Union[str, None] = ${repr(down_revision)}
+branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
+depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ """Upgrade schema."""
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ ${downgrades if downgrades else "pass"}
diff --git a/openmemory/api/alembic/versions/0b53c747049a_initial_migration.py b/openmemory/api/alembic/versions/0b53c747049a_initial_migration.py
new file mode 100644
index 00000000..fb834314
--- /dev/null
+++ b/openmemory/api/alembic/versions/0b53c747049a_initial_migration.py
@@ -0,0 +1,226 @@
+"""Initial migration
+
+Revision ID: 0b53c747049a
+Revises:
+Create Date: 2025-04-19 00:59:56.244203
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '0b53c747049a'
+down_revision: Union[str, None] = None
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ """Upgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('access_controls',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('subject_type', sa.String(), nullable=False),
+ sa.Column('subject_id', sa.UUID(), nullable=True),
+ sa.Column('object_type', sa.String(), nullable=False),
+ sa.Column('object_id', sa.UUID(), nullable=True),
+ sa.Column('effect', sa.String(), nullable=False),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index('idx_access_object', 'access_controls', ['object_type', 'object_id'], unique=False)
+ op.create_index('idx_access_subject', 'access_controls', ['subject_type', 'subject_id'], unique=False)
+ op.create_index(op.f('ix_access_controls_created_at'), 'access_controls', ['created_at'], unique=False)
+ op.create_index(op.f('ix_access_controls_effect'), 'access_controls', ['effect'], unique=False)
+ op.create_index(op.f('ix_access_controls_object_id'), 'access_controls', ['object_id'], unique=False)
+ op.create_index(op.f('ix_access_controls_object_type'), 'access_controls', ['object_type'], unique=False)
+ op.create_index(op.f('ix_access_controls_subject_id'), 'access_controls', ['subject_id'], unique=False)
+ op.create_index(op.f('ix_access_controls_subject_type'), 'access_controls', ['subject_type'], unique=False)
+ op.create_table('archive_policies',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('criteria_type', sa.String(), nullable=False),
+ sa.Column('criteria_id', sa.UUID(), nullable=True),
+ sa.Column('days_to_archive', sa.Integer(), nullable=False),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index('idx_policy_criteria', 'archive_policies', ['criteria_type', 'criteria_id'], unique=False)
+ op.create_index(op.f('ix_archive_policies_created_at'), 'archive_policies', ['created_at'], unique=False)
+ op.create_index(op.f('ix_archive_policies_criteria_id'), 'archive_policies', ['criteria_id'], unique=False)
+ op.create_index(op.f('ix_archive_policies_criteria_type'), 'archive_policies', ['criteria_type'], unique=False)
+ op.create_table('categories',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('name', sa.String(), nullable=False),
+ sa.Column('description', sa.String(), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_categories_created_at'), 'categories', ['created_at'], unique=False)
+ op.create_index(op.f('ix_categories_name'), 'categories', ['name'], unique=True)
+ op.create_table('users',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('user_id', sa.String(), nullable=False),
+ sa.Column('name', sa.String(), nullable=True),
+ sa.Column('email', sa.String(), nullable=True),
+ sa.Column('metadata', sa.JSON(), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_users_created_at'), 'users', ['created_at'], unique=False)
+ op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
+ op.create_index(op.f('ix_users_name'), 'users', ['name'], unique=False)
+ op.create_index(op.f('ix_users_user_id'), 'users', ['user_id'], unique=True)
+ op.create_table('apps',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('owner_id', sa.UUID(), nullable=False),
+ sa.Column('name', sa.String(), nullable=False),
+ sa.Column('description', sa.String(), nullable=True),
+ sa.Column('metadata', sa.JSON(), nullable=True),
+ sa.Column('is_active', sa.Boolean(), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_apps_created_at'), 'apps', ['created_at'], unique=False)
+ op.create_index(op.f('ix_apps_is_active'), 'apps', ['is_active'], unique=False)
+ op.create_index(op.f('ix_apps_name'), 'apps', ['name'], unique=True)
+ op.create_index(op.f('ix_apps_owner_id'), 'apps', ['owner_id'], unique=False)
+ op.create_table('memories',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('user_id', sa.UUID(), nullable=False),
+ sa.Column('app_id', sa.UUID(), nullable=False),
+ sa.Column('content', sa.String(), nullable=False),
+ sa.Column('vector', sa.String(), nullable=True),
+ sa.Column('metadata', sa.JSON(), nullable=True),
+ sa.Column('state', sa.Enum('active', 'paused', 'archived', 'deleted', name='memorystate'), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.Column('archived_at', sa.DateTime(), nullable=True),
+ sa.Column('deleted_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['app_id'], ['apps.id'], ),
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index('idx_memory_app_state', 'memories', ['app_id', 'state'], unique=False)
+ op.create_index('idx_memory_user_app', 'memories', ['user_id', 'app_id'], unique=False)
+ op.create_index('idx_memory_user_state', 'memories', ['user_id', 'state'], unique=False)
+ op.create_index(op.f('ix_memories_app_id'), 'memories', ['app_id'], unique=False)
+ op.create_index(op.f('ix_memories_archived_at'), 'memories', ['archived_at'], unique=False)
+ op.create_index(op.f('ix_memories_created_at'), 'memories', ['created_at'], unique=False)
+ op.create_index(op.f('ix_memories_deleted_at'), 'memories', ['deleted_at'], unique=False)
+ op.create_index(op.f('ix_memories_state'), 'memories', ['state'], unique=False)
+ op.create_index(op.f('ix_memories_user_id'), 'memories', ['user_id'], unique=False)
+ op.create_table('memory_access_logs',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('memory_id', sa.UUID(), nullable=False),
+ sa.Column('app_id', sa.UUID(), nullable=False),
+ sa.Column('accessed_at', sa.DateTime(), nullable=True),
+ sa.Column('access_type', sa.String(), nullable=False),
+ sa.Column('metadata', sa.JSON(), nullable=True),
+ sa.ForeignKeyConstraint(['app_id'], ['apps.id'], ),
+ sa.ForeignKeyConstraint(['memory_id'], ['memories.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index('idx_access_app_time', 'memory_access_logs', ['app_id', 'accessed_at'], unique=False)
+ op.create_index('idx_access_memory_time', 'memory_access_logs', ['memory_id', 'accessed_at'], unique=False)
+ op.create_index(op.f('ix_memory_access_logs_access_type'), 'memory_access_logs', ['access_type'], unique=False)
+ op.create_index(op.f('ix_memory_access_logs_accessed_at'), 'memory_access_logs', ['accessed_at'], unique=False)
+ op.create_index(op.f('ix_memory_access_logs_app_id'), 'memory_access_logs', ['app_id'], unique=False)
+ op.create_index(op.f('ix_memory_access_logs_memory_id'), 'memory_access_logs', ['memory_id'], unique=False)
+ op.create_table('memory_categories',
+ sa.Column('memory_id', sa.UUID(), nullable=False),
+ sa.Column('category_id', sa.UUID(), nullable=False),
+ sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ),
+ sa.ForeignKeyConstraint(['memory_id'], ['memories.id'], ),
+ sa.PrimaryKeyConstraint('memory_id', 'category_id')
+ )
+ op.create_index('idx_memory_category', 'memory_categories', ['memory_id', 'category_id'], unique=False)
+ op.create_index(op.f('ix_memory_categories_category_id'), 'memory_categories', ['category_id'], unique=False)
+ op.create_index(op.f('ix_memory_categories_memory_id'), 'memory_categories', ['memory_id'], unique=False)
+ op.create_table('memory_status_history',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('memory_id', sa.UUID(), nullable=False),
+ sa.Column('changed_by', sa.UUID(), nullable=False),
+ sa.Column('old_state', sa.Enum('active', 'paused', 'archived', 'deleted', name='memorystate'), nullable=False),
+ sa.Column('new_state', sa.Enum('active', 'paused', 'archived', 'deleted', name='memorystate'), nullable=False),
+ sa.Column('changed_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['changed_by'], ['users.id'], ),
+ sa.ForeignKeyConstraint(['memory_id'], ['memories.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index('idx_history_memory_state', 'memory_status_history', ['memory_id', 'new_state'], unique=False)
+ op.create_index('idx_history_user_time', 'memory_status_history', ['changed_by', 'changed_at'], unique=False)
+ op.create_index(op.f('ix_memory_status_history_changed_at'), 'memory_status_history', ['changed_at'], unique=False)
+ op.create_index(op.f('ix_memory_status_history_changed_by'), 'memory_status_history', ['changed_by'], unique=False)
+ op.create_index(op.f('ix_memory_status_history_memory_id'), 'memory_status_history', ['memory_id'], unique=False)
+ op.create_index(op.f('ix_memory_status_history_new_state'), 'memory_status_history', ['new_state'], unique=False)
+ op.create_index(op.f('ix_memory_status_history_old_state'), 'memory_status_history', ['old_state'], unique=False)
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f('ix_memory_status_history_old_state'), table_name='memory_status_history')
+ op.drop_index(op.f('ix_memory_status_history_new_state'), table_name='memory_status_history')
+ op.drop_index(op.f('ix_memory_status_history_memory_id'), table_name='memory_status_history')
+ op.drop_index(op.f('ix_memory_status_history_changed_by'), table_name='memory_status_history')
+ op.drop_index(op.f('ix_memory_status_history_changed_at'), table_name='memory_status_history')
+ op.drop_index('idx_history_user_time', table_name='memory_status_history')
+ op.drop_index('idx_history_memory_state', table_name='memory_status_history')
+ op.drop_table('memory_status_history')
+ op.drop_index(op.f('ix_memory_categories_memory_id'), table_name='memory_categories')
+ op.drop_index(op.f('ix_memory_categories_category_id'), table_name='memory_categories')
+ op.drop_index('idx_memory_category', table_name='memory_categories')
+ op.drop_table('memory_categories')
+ op.drop_index(op.f('ix_memory_access_logs_memory_id'), table_name='memory_access_logs')
+ op.drop_index(op.f('ix_memory_access_logs_app_id'), table_name='memory_access_logs')
+ op.drop_index(op.f('ix_memory_access_logs_accessed_at'), table_name='memory_access_logs')
+ op.drop_index(op.f('ix_memory_access_logs_access_type'), table_name='memory_access_logs')
+ op.drop_index('idx_access_memory_time', table_name='memory_access_logs')
+ op.drop_index('idx_access_app_time', table_name='memory_access_logs')
+ op.drop_table('memory_access_logs')
+ op.drop_index(op.f('ix_memories_user_id'), table_name='memories')
+ op.drop_index(op.f('ix_memories_state'), table_name='memories')
+ op.drop_index(op.f('ix_memories_deleted_at'), table_name='memories')
+ op.drop_index(op.f('ix_memories_created_at'), table_name='memories')
+ op.drop_index(op.f('ix_memories_archived_at'), table_name='memories')
+ op.drop_index(op.f('ix_memories_app_id'), table_name='memories')
+ op.drop_index('idx_memory_user_state', table_name='memories')
+ op.drop_index('idx_memory_user_app', table_name='memories')
+ op.drop_index('idx_memory_app_state', table_name='memories')
+ op.drop_table('memories')
+ op.drop_index(op.f('ix_apps_owner_id'), table_name='apps')
+ op.drop_index(op.f('ix_apps_name'), table_name='apps')
+ op.drop_index(op.f('ix_apps_is_active'), table_name='apps')
+ op.drop_index(op.f('ix_apps_created_at'), table_name='apps')
+ op.drop_table('apps')
+ op.drop_index(op.f('ix_users_user_id'), table_name='users')
+ op.drop_index(op.f('ix_users_name'), table_name='users')
+ op.drop_index(op.f('ix_users_email'), table_name='users')
+ op.drop_index(op.f('ix_users_created_at'), table_name='users')
+ op.drop_table('users')
+ op.drop_index(op.f('ix_categories_name'), table_name='categories')
+ op.drop_index(op.f('ix_categories_created_at'), table_name='categories')
+ op.drop_table('categories')
+ op.drop_index(op.f('ix_archive_policies_criteria_type'), table_name='archive_policies')
+ op.drop_index(op.f('ix_archive_policies_criteria_id'), table_name='archive_policies')
+ op.drop_index(op.f('ix_archive_policies_created_at'), table_name='archive_policies')
+ op.drop_index('idx_policy_criteria', table_name='archive_policies')
+ op.drop_table('archive_policies')
+ op.drop_index(op.f('ix_access_controls_subject_type'), table_name='access_controls')
+ op.drop_index(op.f('ix_access_controls_subject_id'), table_name='access_controls')
+ op.drop_index(op.f('ix_access_controls_object_type'), table_name='access_controls')
+ op.drop_index(op.f('ix_access_controls_object_id'), table_name='access_controls')
+ op.drop_index(op.f('ix_access_controls_effect'), table_name='access_controls')
+ op.drop_index(op.f('ix_access_controls_created_at'), table_name='access_controls')
+ op.drop_index('idx_access_subject', table_name='access_controls')
+ op.drop_index('idx_access_object', table_name='access_controls')
+ op.drop_table('access_controls')
+ # ### end Alembic commands ###
diff --git a/openmemory/api/app/__init__.py b/openmemory/api/app/__init__.py
new file mode 100644
index 00000000..4c4d1566
--- /dev/null
+++ b/openmemory/api/app/__init__.py
@@ -0,0 +1 @@
+# This file makes the app directory a Python package
\ No newline at end of file
diff --git a/openmemory/api/app/config.py b/openmemory/api/app/config.py
new file mode 100644
index 00000000..5da45423
--- /dev/null
+++ b/openmemory/api/app/config.py
@@ -0,0 +1,4 @@
+import os
+
+USER_ID = os.getenv("USER", "default_user")
+DEFAULT_APP_ID = "openmemory"
\ No newline at end of file
diff --git a/openmemory/api/app/database.py b/openmemory/api/app/database.py
new file mode 100644
index 00000000..6404ce70
--- /dev/null
+++ b/openmemory/api/app/database.py
@@ -0,0 +1,29 @@
+import os
+from sqlalchemy import create_engine
+from sqlalchemy.orm import declarative_base, sessionmaker
+from dotenv import load_dotenv
+
+# load .env file (make sure you have DATABASE_URL set)
+load_dotenv()
+
+DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./openmemory.db")
+if not DATABASE_URL:
+ raise RuntimeError("DATABASE_URL is not set in environment")
+
+# SQLAlchemy engine & session
+engine = create_engine(
+ DATABASE_URL,
+ connect_args={"check_same_thread": False} # Needed for SQLite
+)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+# Base class for models
+Base = declarative_base()
+
+# Dependency for FastAPI
+def get_db():
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
diff --git a/openmemory/api/app/mcp_server.py b/openmemory/api/app/mcp_server.py
new file mode 100644
index 00000000..e5774590
--- /dev/null
+++ b/openmemory/api/app/mcp_server.py
@@ -0,0 +1,382 @@
+import logging
+import json
+from mcp.server.fastmcp import FastMCP
+from mcp.server.sse import SseServerTransport
+from app.utils.memory import get_memory_client
+from fastapi import FastAPI, Request
+from fastapi.routing import APIRouter
+import contextvars
+import os
+from dotenv import load_dotenv
+from app.database import SessionLocal
+from app.models import Memory, MemoryState, MemoryStatusHistory, MemoryAccessLog
+from app.utils.db import get_user_and_app
+import uuid
+import datetime
+from app.utils.permissions import check_memory_access_permissions
+from qdrant_client import models as qdrant_models
+
+# Load environment variables
+load_dotenv()
+
+# Initialize MCP and memory client
+mcp = FastMCP("mem0-mcp-server")
+
+# Check if OpenAI API key is set
+if not os.getenv("OPENAI_API_KEY"):
+ raise Exception("OPENAI_API_KEY is not set in .env file")
+
+memory_client = get_memory_client()
+
+# Context variables for user_id and client_name
+user_id_var: contextvars.ContextVar[str] = contextvars.ContextVar("user_id")
+client_name_var: contextvars.ContextVar[str] = contextvars.ContextVar("client_name")
+
+# Create a router for MCP endpoints
+mcp_router = APIRouter(prefix="/mcp")
+
+# Initialize SSE transport
+sse = SseServerTransport("/mcp/messages/")
+
+@mcp.tool(description="Add new memories to the user's memory")
+async def add_memories(text: str) -> str:
+ uid = user_id_var.get(None)
+ client_name = client_name_var.get(None)
+
+ if not uid:
+ return "Error: user_id not provided"
+ if not client_name:
+ return "Error: client_name not provided"
+
+ try:
+ db = SessionLocal()
+ try:
+ # Get or create user and app
+ user, app = get_user_and_app(db, user_id=uid, app_id=client_name)
+
+ # Check if app is active
+ if not app.is_active:
+ return f"Error: App {app.name} is currently paused on OpenMemory. Cannot create new memories."
+
+ response = memory_client.add(text,
+ user_id=uid,
+ metadata={
+ "source_app": "openmemory",
+ "mcp_client": client_name,
+ })
+
+ # Process the response and update database
+ if isinstance(response, dict) and 'results' in response:
+ for result in response['results']:
+ memory_id = uuid.UUID(result['id'])
+ memory = db.query(Memory).filter(Memory.id == memory_id).first()
+
+ if result['event'] == 'ADD':
+ if not memory:
+ memory = Memory(
+ id=memory_id,
+ user_id=user.id,
+ app_id=app.id,
+ content=result['memory'],
+ state=MemoryState.active
+ )
+ db.add(memory)
+ else:
+ memory.state = MemoryState.active
+ memory.content = result['memory']
+
+ # Create history entry
+ history = MemoryStatusHistory(
+ memory_id=memory_id,
+ changed_by=user.id,
+ old_state=MemoryState.deleted if memory else None,
+ new_state=MemoryState.active
+ )
+ db.add(history)
+
+ elif result['event'] == 'DELETE':
+ if memory:
+ memory.state = MemoryState.deleted
+ memory.deleted_at = datetime.datetime.now(datetime.UTC)
+ # Create history entry
+ history = MemoryStatusHistory(
+ memory_id=memory_id,
+ changed_by=user.id,
+ old_state=MemoryState.active,
+ new_state=MemoryState.deleted
+ )
+ db.add(history)
+
+ db.commit()
+
+ return response
+ finally:
+ db.close()
+ except Exception as e:
+ return f"Error adding to memory: {e}"
+
+
+@mcp.tool(description="Search the user's memory for memories that match the query")
+async def search_memory(query: str) -> str:
+ uid = user_id_var.get(None)
+ client_name = client_name_var.get(None)
+ if not uid:
+ return "Error: user_id not provided"
+ if not client_name:
+ return "Error: client_name not provided"
+ try:
+ db = SessionLocal()
+ try:
+ # Get or create user and app
+ user, app = get_user_and_app(db, user_id=uid, app_id=client_name)
+
+ # Get accessible memory IDs based on ACL
+ user_memories = db.query(Memory).filter(Memory.user_id == user.id).all()
+ accessible_memory_ids = [memory.id for memory in user_memories if check_memory_access_permissions(db, memory, app.id)]
+ conditions = [qdrant_models.FieldCondition(key="user_id", match=qdrant_models.MatchValue(value=uid))]
+ logging.info(f"Accessible memory IDs: {accessible_memory_ids}")
+ logging.info(f"Conditions: {conditions}")
+ if accessible_memory_ids:
+ # Convert UUIDs to strings for Qdrant
+ accessible_memory_ids_str = [str(memory_id) for memory_id in accessible_memory_ids]
+ conditions.append(qdrant_models.HasIdCondition(has_id=accessible_memory_ids_str))
+ filters = qdrant_models.Filter(must=conditions)
+ logging.info(f"Filters: {filters}")
+ embeddings = memory_client.embedding_model.embed(query, "search")
+ hits = memory_client.vector_store.client.query_points(
+ collection_name=memory_client.vector_store.collection_name,
+ query=embeddings,
+ query_filter=filters,
+ limit=10,
+ )
+
+ memories = hits.points
+ memories = [
+ {
+ "id": memory.id,
+ "memory": memory.payload["data"],
+ "hash": memory.payload.get("hash"),
+ "created_at": memory.payload.get("created_at"),
+ "updated_at": memory.payload.get("updated_at"),
+ "score": memory.score,
+ }
+ for memory in memories
+ ]
+
+ # Log memory access for each memory found
+ if isinstance(memories, dict) and 'results' in memories:
+ print(f"Memories: {memories}")
+ for memory_data in memories['results']:
+ if 'id' in memory_data:
+ memory_id = uuid.UUID(memory_data['id'])
+ # Create access log entry
+ access_log = MemoryAccessLog(
+ memory_id=memory_id,
+ app_id=app.id,
+ access_type="search",
+ metadata_={
+ "query": query,
+ "score": memory_data.get('score'),
+ "hash": memory_data.get('hash')
+ }
+ )
+ db.add(access_log)
+ db.commit()
+ else:
+ for memory in memories:
+ memory_id = uuid.UUID(memory['id'])
+ # Create access log entry
+ access_log = MemoryAccessLog(
+ memory_id=memory_id,
+ app_id=app.id,
+ access_type="search",
+ metadata_={
+ "query": query,
+ "score": memory.get('score'),
+ "hash": memory.get('hash')
+ }
+ )
+ db.add(access_log)
+ db.commit()
+ return json.dumps(memories, indent=2)
+ finally:
+ db.close()
+ except Exception as e:
+ logging.exception(e)
+ return f"Error searching memory: {e}"
+
+
+@mcp.tool(description="List all memories in the user's memory")
+async def list_memories() -> str:
+ uid = user_id_var.get(None)
+ client_name = client_name_var.get(None)
+ if not uid:
+ return "Error: user_id not provided"
+ if not client_name:
+ return "Error: client_name not provided"
+ try:
+ db = SessionLocal()
+ try:
+ # Get or create user and app
+ user, app = get_user_and_app(db, user_id=uid, app_id=client_name)
+
+ # Get all memories
+ memories = memory_client.get_all(user_id=uid)
+ filtered_memories = []
+
+ # Filter memories based on permissions
+ user_memories = db.query(Memory).filter(Memory.user_id == user.id).all()
+ accessible_memory_ids = [memory.id for memory in user_memories if check_memory_access_permissions(db, memory, app.id)]
+ if isinstance(memories, dict) and 'results' in memories:
+ for memory_data in memories['results']:
+ if 'id' in memory_data:
+ memory_id = uuid.UUID(memory_data['id'])
+ if memory_id in accessible_memory_ids:
+ # Create access log entry
+ access_log = MemoryAccessLog(
+ memory_id=memory_id,
+ app_id=app.id,
+ access_type="list",
+ metadata_={
+ "hash": memory_data.get('hash')
+ }
+ )
+ db.add(access_log)
+ filtered_memories.append(memory_data)
+ db.commit()
+ else:
+ for memory in memories:
+ memory_id = uuid.UUID(memory['id'])
+ memory_obj = db.query(Memory).filter(Memory.id == memory_id).first()
+ if memory_obj and check_memory_access_permissions(db, memory_obj, app.id):
+ # Create access log entry
+ access_log = MemoryAccessLog(
+ memory_id=memory_id,
+ app_id=app.id,
+ access_type="list",
+ metadata_={
+ "hash": memory.get('hash')
+ }
+ )
+ db.add(access_log)
+ filtered_memories.append(memory)
+ db.commit()
+ return json.dumps(filtered_memories, indent=2)
+ finally:
+ db.close()
+ except Exception as e:
+ return f"Error getting memories: {e}"
+
+
+@mcp.tool(description="Delete all memories in the user's memory")
+async def delete_all_memories() -> str:
+ uid = user_id_var.get(None)
+ client_name = client_name_var.get(None)
+ if not uid:
+ return "Error: user_id not provided"
+ if not client_name:
+ return "Error: client_name not provided"
+ try:
+ db = SessionLocal()
+ try:
+ # Get or create user and app
+ user, app = get_user_and_app(db, user_id=uid, app_id=client_name)
+
+ user_memories = db.query(Memory).filter(Memory.user_id == user.id).all()
+ accessible_memory_ids = [memory.id for memory in user_memories if check_memory_access_permissions(db, memory, app.id)]
+
+ # delete the accessible memories only
+ for memory_id in accessible_memory_ids:
+ memory_client.delete(memory_id)
+
+ # Update each memory's state and create history entries
+ now = datetime.datetime.now(datetime.UTC)
+ for memory_id in accessible_memory_ids:
+ memory = db.query(Memory).filter(Memory.id == memory_id).first()
+ # Update memory state
+ memory.state = MemoryState.deleted
+ memory.deleted_at = now
+
+ # Create history entry
+ history = MemoryStatusHistory(
+ memory_id=memory_id,
+ changed_by=user.id,
+ old_state=MemoryState.active,
+ new_state=MemoryState.deleted
+ )
+ db.add(history)
+
+ # Create access log entry
+ access_log = MemoryAccessLog(
+ memory_id=memory_id,
+ app_id=app.id,
+ access_type="delete_all",
+ metadata_={"operation": "bulk_delete"}
+ )
+ db.add(access_log)
+
+ db.commit()
+ return "Successfully deleted all memories"
+ finally:
+ db.close()
+ except Exception as e:
+ return f"Error deleting memories: {e}"
+
+
+@mcp_router.get("/{client_name}/sse/{user_id}")
+async def handle_sse(request: Request):
+ """Handle SSE connections for a specific user and client"""
+ # Extract user_id and client_name from path parameters
+ uid = request.path_params.get("user_id")
+ user_token = user_id_var.set(uid or "")
+ client_name = request.path_params.get("client_name")
+ client_token = client_name_var.set(client_name or "")
+
+ try:
+ # Handle SSE connection
+ async with sse.connect_sse(
+ request.scope,
+ request.receive,
+ request._send,
+ ) as (read_stream, write_stream):
+ await mcp._mcp_server.run(
+ read_stream,
+ write_stream,
+ mcp._mcp_server.create_initialization_options(),
+ )
+ finally:
+ # Clean up context variables
+ user_id_var.reset(user_token)
+ client_name_var.reset(client_token)
+
+
+@mcp_router.post("/messages/")
+async def handle_post_message(request: Request):
+ """Handle POST messages for SSE"""
+ try:
+ body = await request.body()
+
+ # Create a simple receive function that returns the body
+ async def receive():
+ return {"type": "http.request", "body": body, "more_body": False}
+
+ # Create a simple send function that does nothing
+ async def send(message):
+ pass
+
+ # Call handle_post_message with the correct arguments
+ await sse.handle_post_message(request.scope, receive, send)
+
+ # Return a success response
+ return {"status": "ok"}
+ finally:
+ pass
+ # Clean up context variable
+ # client_name_var.reset(client_token)
+
+def setup_mcp_server(app: FastAPI):
+ """Setup MCP server with the FastAPI application"""
+ mcp._mcp_server.name = f"mem0-mcp-server"
+
+ # Include MCP router in the FastAPI app
+ app.include_router(mcp_router)
diff --git a/openmemory/api/app/models.py b/openmemory/api/app/models.py
new file mode 100644
index 00000000..b41aa4ed
--- /dev/null
+++ b/openmemory/api/app/models.py
@@ -0,0 +1,217 @@
+import enum
+import uuid
+import datetime
+from sqlalchemy import (
+ Column, String, Boolean, ForeignKey, Enum, Table,
+ DateTime, JSON, Integer, UUID, Index, event
+)
+from sqlalchemy.orm import relationship
+from app.database import Base
+from sqlalchemy.orm import Session
+from app.utils.categorization import get_categories_for_memory
+
+
+def get_current_utc_time():
+ """Get current UTC time"""
+ return datetime.datetime.now(datetime.UTC)
+
+
+class MemoryState(enum.Enum):
+ active = "active"
+ paused = "paused"
+ archived = "archived"
+ deleted = "deleted"
+
+
+class User(Base):
+ __tablename__ = "users"
+ id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
+ user_id = Column(String, nullable=False, unique=True, index=True)
+ name = Column(String, nullable=True, index=True)
+ email = Column(String, unique=True, nullable=True, index=True)
+ metadata_ = Column('metadata', JSON, default=dict)
+ created_at = Column(DateTime, default=get_current_utc_time, index=True)
+ updated_at = Column(DateTime,
+ default=get_current_utc_time,
+ onupdate=get_current_utc_time)
+
+ apps = relationship("App", back_populates="owner")
+ memories = relationship("Memory", back_populates="user")
+
+
+class App(Base):
+ __tablename__ = "apps"
+ id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
+ owner_id = Column(UUID, ForeignKey("users.id"), nullable=False, index=True)
+ name = Column(String, unique=True, nullable=False, index=True)
+ description = Column(String)
+ metadata_ = Column('metadata', JSON, default=dict)
+ is_active = Column(Boolean, default=True, index=True)
+ created_at = Column(DateTime, default=get_current_utc_time, index=True)
+ updated_at = Column(DateTime,
+ default=get_current_utc_time,
+ onupdate=get_current_utc_time)
+
+ owner = relationship("User", back_populates="apps")
+ memories = relationship("Memory", back_populates="app")
+
+
+class Memory(Base):
+ __tablename__ = "memories"
+ id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
+ user_id = Column(UUID, ForeignKey("users.id"), nullable=False, index=True)
+ app_id = Column(UUID, ForeignKey("apps.id"), nullable=False, index=True)
+ content = Column(String, nullable=False)
+ vector = Column(String)
+ metadata_ = Column('metadata', JSON, default=dict)
+ state = Column(Enum(MemoryState), default=MemoryState.active, index=True)
+ created_at = Column(DateTime, default=get_current_utc_time, index=True)
+ updated_at = Column(DateTime,
+ default=get_current_utc_time,
+ onupdate=get_current_utc_time)
+ archived_at = Column(DateTime, nullable=True, index=True)
+ deleted_at = Column(DateTime, nullable=True, index=True)
+
+ user = relationship("User", back_populates="memories")
+ app = relationship("App", back_populates="memories")
+ categories = relationship("Category", secondary="memory_categories", back_populates="memories")
+
+ __table_args__ = (
+ Index('idx_memory_user_state', 'user_id', 'state'),
+ Index('idx_memory_app_state', 'app_id', 'state'),
+ Index('idx_memory_user_app', 'user_id', 'app_id'),
+ )
+
+
+class Category(Base):
+ __tablename__ = "categories"
+ id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
+ name = Column(String, unique=True, nullable=False, index=True)
+ description = Column(String)
+ created_at = Column(DateTime, default=datetime.datetime.now(datetime.UTC), index=True)
+ updated_at = Column(DateTime,
+ default=get_current_utc_time,
+ onupdate=get_current_utc_time)
+
+ memories = relationship("Memory", secondary="memory_categories", back_populates="categories")
+
+memory_categories = Table(
+ "memory_categories", Base.metadata,
+ Column("memory_id", UUID, ForeignKey("memories.id"), primary_key=True, index=True),
+ Column("category_id", UUID, ForeignKey("categories.id"), primary_key=True, index=True),
+ Index('idx_memory_category', 'memory_id', 'category_id')
+)
+
+
+class AccessControl(Base):
+ __tablename__ = "access_controls"
+ id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
+ subject_type = Column(String, nullable=False, index=True)
+ subject_id = Column(UUID, nullable=True, index=True)
+ object_type = Column(String, nullable=False, index=True)
+ object_id = Column(UUID, nullable=True, index=True)
+ effect = Column(String, nullable=False, index=True)
+ created_at = Column(DateTime, default=get_current_utc_time, index=True)
+
+ __table_args__ = (
+ Index('idx_access_subject', 'subject_type', 'subject_id'),
+ Index('idx_access_object', 'object_type', 'object_id'),
+ )
+
+
+class ArchivePolicy(Base):
+ __tablename__ = "archive_policies"
+ id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
+ criteria_type = Column(String, nullable=False, index=True)
+ criteria_id = Column(UUID, nullable=True, index=True)
+ days_to_archive = Column(Integer, nullable=False)
+ created_at = Column(DateTime, default=get_current_utc_time, index=True)
+
+ __table_args__ = (
+ Index('idx_policy_criteria', 'criteria_type', 'criteria_id'),
+ )
+
+
+class MemoryStatusHistory(Base):
+ __tablename__ = "memory_status_history"
+ id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
+ memory_id = Column(UUID, ForeignKey("memories.id"), nullable=False, index=True)
+ changed_by = Column(UUID, ForeignKey("users.id"), nullable=False, index=True)
+ old_state = Column(Enum(MemoryState), nullable=False, index=True)
+ new_state = Column(Enum(MemoryState), nullable=False, index=True)
+ changed_at = Column(DateTime, default=get_current_utc_time, index=True)
+
+ __table_args__ = (
+ Index('idx_history_memory_state', 'memory_id', 'new_state'),
+ Index('idx_history_user_time', 'changed_by', 'changed_at'),
+ )
+
+
+class MemoryAccessLog(Base):
+ __tablename__ = "memory_access_logs"
+ id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
+ memory_id = Column(UUID, ForeignKey("memories.id"), nullable=False, index=True)
+ app_id = Column(UUID, ForeignKey("apps.id"), nullable=False, index=True)
+ accessed_at = Column(DateTime, default=get_current_utc_time, index=True)
+ access_type = Column(String, nullable=False, index=True)
+ metadata_ = Column('metadata', JSON, default=dict)
+
+ __table_args__ = (
+ Index('idx_access_memory_time', 'memory_id', 'accessed_at'),
+ Index('idx_access_app_time', 'app_id', 'accessed_at'),
+ )
+
+def categorize_memory(memory: Memory, db: Session) -> None:
+ """Categorize a memory using OpenAI and store the categories in the database."""
+ try:
+ # Get categories from OpenAI
+ categories = get_categories_for_memory(memory.content)
+
+ # Get or create categories in the database
+ for category_name in categories:
+ category = db.query(Category).filter(Category.name == category_name).first()
+ if not category:
+ category = Category(
+ name=category_name,
+ description=f"Automatically created category for {category_name}"
+ )
+ db.add(category)
+ db.flush() # Flush to get the category ID
+
+ # Check if the memory-category association already exists
+ existing = db.execute(
+ memory_categories.select().where(
+ (memory_categories.c.memory_id == memory.id) &
+ (memory_categories.c.category_id == category.id)
+ )
+ ).first()
+
+ if not existing:
+ # Create the association
+ db.execute(
+ memory_categories.insert().values(
+ memory_id=memory.id,
+ category_id=category.id
+ )
+ )
+
+ db.commit()
+ except Exception as e:
+ db.rollback()
+ print(f"Error categorizing memory: {e}")
+
+
+@event.listens_for(Memory, 'after_insert')
+def after_memory_insert(mapper, connection, target):
+ """Trigger categorization after a memory is inserted."""
+ db = Session(bind=connection)
+ categorize_memory(target, db)
+ db.close()
+
+
+@event.listens_for(Memory, 'after_update')
+def after_memory_update(mapper, connection, target):
+ """Trigger categorization after a memory is updated."""
+ db = Session(bind=connection)
+ categorize_memory(target, db)
+ db.close()
diff --git a/openmemory/api/app/routers/__init__.py b/openmemory/api/app/routers/__init__.py
new file mode 100644
index 00000000..f152fc2e
--- /dev/null
+++ b/openmemory/api/app/routers/__init__.py
@@ -0,0 +1,5 @@
+from .memories import router as memories_router
+from .apps import router as apps_router
+from .stats import router as stats_router
+
+__all__ = ["memories_router", "apps_router", "stats_router"]
\ No newline at end of file
diff --git a/openmemory/api/app/routers/apps.py b/openmemory/api/app/routers/apps.py
new file mode 100644
index 00000000..36584f92
--- /dev/null
+++ b/openmemory/api/app/routers/apps.py
@@ -0,0 +1,223 @@
+from typing import Optional
+from uuid import UUID
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy.orm import Session, joinedload
+from sqlalchemy import func, desc
+
+from app.database import get_db
+from app.models import App, Memory, MemoryAccessLog, MemoryState
+
+router = APIRouter(prefix="/api/v1/apps", tags=["apps"])
+
+# Helper functions
+def get_app_or_404(db: Session, app_id: UUID) -> App:
+ app = db.query(App).filter(App.id == app_id).first()
+ if not app:
+ raise HTTPException(status_code=404, detail="App not found")
+ return app
+
+# List all apps with filtering
+@router.get("/")
+async def list_apps(
+ name: Optional[str] = None,
+ is_active: Optional[bool] = None,
+ sort_by: str = 'name',
+ sort_direction: str = 'asc',
+ page: int = Query(1, ge=1),
+ page_size: int = Query(10, ge=1, le=100),
+ db: Session = Depends(get_db)
+):
+ # Create a subquery for memory counts
+ memory_counts = db.query(
+ Memory.app_id,
+ func.count(Memory.id).label('memory_count')
+ ).filter(
+ Memory.state.in_([MemoryState.active, MemoryState.paused, MemoryState.archived])
+ ).group_by(Memory.app_id).subquery()
+
+ # Create a subquery for access counts
+ access_counts = db.query(
+ MemoryAccessLog.app_id,
+ func.count(func.distinct(MemoryAccessLog.memory_id)).label('access_count')
+ ).group_by(MemoryAccessLog.app_id).subquery()
+
+ # Base query
+ query = db.query(
+ App,
+ func.coalesce(memory_counts.c.memory_count, 0).label('total_memories_created'),
+ func.coalesce(access_counts.c.access_count, 0).label('total_memories_accessed')
+ )
+
+ # Join with subqueries
+ query = query.outerjoin(
+ memory_counts,
+ App.id == memory_counts.c.app_id
+ ).outerjoin(
+ access_counts,
+ App.id == access_counts.c.app_id
+ )
+
+ if name:
+ query = query.filter(App.name.ilike(f"%{name}%"))
+
+ if is_active is not None:
+ query = query.filter(App.is_active == is_active)
+
+ # Apply sorting
+ if sort_by == 'name':
+ sort_field = App.name
+ elif sort_by == 'memories':
+ sort_field = func.coalesce(memory_counts.c.memory_count, 0)
+ elif sort_by == 'memories_accessed':
+ sort_field = func.coalesce(access_counts.c.access_count, 0)
+ else:
+ sort_field = App.name # default sort
+
+ if sort_direction == 'desc':
+ query = query.order_by(desc(sort_field))
+ else:
+ query = query.order_by(sort_field)
+
+ total = query.count()
+ apps = query.offset((page - 1) * page_size).limit(page_size).all()
+
+ return {
+ "total": total,
+ "page": page,
+ "page_size": page_size,
+ "apps": [
+ {
+ "id": app[0].id,
+ "name": app[0].name,
+ "is_active": app[0].is_active,
+ "total_memories_created": app[1],
+ "total_memories_accessed": app[2]
+ }
+ for app in apps
+ ]
+ }
+
+# Get app details
+@router.get("/{app_id}")
+async def get_app_details(
+ app_id: UUID,
+ db: Session = Depends(get_db)
+):
+ app = get_app_or_404(db, app_id)
+
+ # Get memory access statistics
+ access_stats = db.query(
+ func.count(MemoryAccessLog.id).label("total_memories_accessed"),
+ func.min(MemoryAccessLog.accessed_at).label("first_accessed"),
+ func.max(MemoryAccessLog.accessed_at).label("last_accessed")
+ ).filter(MemoryAccessLog.app_id == app_id).first()
+
+ return {
+ "is_active": app.is_active,
+ "total_memories_created": db.query(Memory)
+ .filter(Memory.app_id == app_id)
+ .count(),
+ "total_memories_accessed": access_stats.total_memories_accessed or 0,
+ "first_accessed": access_stats.first_accessed,
+ "last_accessed": access_stats.last_accessed
+ }
+
+# List memories created by app
+@router.get("/{app_id}/memories")
+async def list_app_memories(
+ app_id: UUID,
+ page: int = Query(1, ge=1),
+ page_size: int = Query(10, ge=1, le=100),
+ db: Session = Depends(get_db)
+):
+ get_app_or_404(db, app_id)
+ query = db.query(Memory).filter(
+ Memory.app_id == app_id,
+ Memory.state.in_([MemoryState.active, MemoryState.paused, MemoryState.archived])
+ )
+ # Add eager loading for categories
+ query = query.options(joinedload(Memory.categories))
+ total = query.count()
+ memories = query.order_by(Memory.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
+
+ return {
+ "total": total,
+ "page": page,
+ "page_size": page_size,
+ "memories": [
+ {
+ "id": memory.id,
+ "content": memory.content,
+ "created_at": memory.created_at,
+ "state": memory.state.value,
+ "app_id": memory.app_id,
+ "categories": [category.name for category in memory.categories],
+ "metadata_": memory.metadata_
+ }
+ for memory in memories
+ ]
+ }
+
+# List memories accessed by app
+@router.get("/{app_id}/accessed")
+async def list_app_accessed_memories(
+ app_id: UUID,
+ page: int = Query(1, ge=1),
+ page_size: int = Query(10, ge=1, le=100),
+ db: Session = Depends(get_db)
+):
+
+ # Get memories with access counts
+ query = db.query(
+ Memory,
+ func.count(MemoryAccessLog.id).label("access_count")
+ ).join(
+ MemoryAccessLog,
+ Memory.id == MemoryAccessLog.memory_id
+ ).filter(
+ MemoryAccessLog.app_id == app_id
+ ).group_by(
+ Memory.id
+ ).order_by(
+ desc("access_count")
+ )
+
+ # Add eager loading for categories
+ query = query.options(joinedload(Memory.categories))
+
+ total = query.count()
+ results = query.offset((page - 1) * page_size).limit(page_size).all()
+
+ return {
+ "total": total,
+ "page": page,
+ "page_size": page_size,
+ "memories": [
+ {
+ "memory": {
+ "id": memory.id,
+ "content": memory.content,
+ "created_at": memory.created_at,
+ "state": memory.state.value,
+ "app_id": memory.app_id,
+ "app_name": memory.app.name if memory.app else None,
+ "categories": [category.name for category in memory.categories],
+ "metadata_": memory.metadata_
+ },
+ "access_count": count
+ }
+ for memory, count in results
+ ]
+ }
+
+
+@router.put("/{app_id}")
+async def update_app_details(
+ app_id: UUID,
+ is_active: bool,
+ db: Session = Depends(get_db)
+):
+ app = get_app_or_404(db, app_id)
+ app.is_active = is_active
+ db.commit()
+ return {"status": "success", "message": "Updated app details successfully"}
diff --git a/openmemory/api/app/routers/memories.py b/openmemory/api/app/routers/memories.py
new file mode 100644
index 00000000..29a942db
--- /dev/null
+++ b/openmemory/api/app/routers/memories.py
@@ -0,0 +1,575 @@
+from datetime import datetime, UTC
+from typing import List, Optional, Set
+from uuid import UUID
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy.orm import Session, joinedload
+from fastapi_pagination import Page, Params
+from fastapi_pagination.ext.sqlalchemy import paginate as sqlalchemy_paginate
+from pydantic import BaseModel
+from sqlalchemy import or_, func
+
+from app.database import get_db
+from app.models import (
+ Memory, MemoryState, MemoryAccessLog, App,
+ MemoryStatusHistory, User, Category, AccessControl
+)
+from app.schemas import MemoryResponse, PaginatedMemoryResponse
+from app.utils.permissions import check_memory_access_permissions
+
+router = APIRouter(prefix="/api/v1/memories", tags=["memories"])
+
+
+def get_memory_or_404(db: Session, memory_id: UUID) -> Memory:
+ memory = db.query(Memory).filter(Memory.id == memory_id).first()
+ if not memory:
+ raise HTTPException(status_code=404, detail="Memory not found")
+ return memory
+
+
+def update_memory_state(db: Session, memory_id: UUID, new_state: MemoryState, user_id: UUID):
+ memory = get_memory_or_404(db, memory_id)
+ old_state = memory.state
+
+ # Update memory state
+ memory.state = new_state
+ if new_state == MemoryState.archived:
+ memory.archived_at = datetime.now(UTC)
+ elif new_state == MemoryState.deleted:
+ memory.deleted_at = datetime.now(UTC)
+
+ # Record state change
+ history = MemoryStatusHistory(
+ memory_id=memory_id,
+ changed_by=user_id,
+ old_state=old_state,
+ new_state=new_state
+ )
+ db.add(history)
+ db.commit()
+ return memory
+
+
+def get_accessible_memory_ids(db: Session, app_id: UUID) -> Set[UUID]:
+ """
+ Get the set of memory IDs that the app has access to based on app-level ACL rules.
+ Returns all memory IDs if no specific restrictions are found.
+ """
+ # Get app-level access controls
+ app_access = db.query(AccessControl).filter(
+ AccessControl.subject_type == "app",
+ AccessControl.subject_id == app_id,
+ AccessControl.object_type == "memory"
+ ).all()
+
+ # If no app-level rules exist, return None to indicate all memories are accessible
+ if not app_access:
+ return None
+
+ # Initialize sets for allowed and denied memory IDs
+ allowed_memory_ids = set()
+ denied_memory_ids = set()
+
+ # Process app-level rules
+ for rule in app_access:
+ if rule.effect == "allow":
+ if rule.object_id: # Specific memory access
+ allowed_memory_ids.add(rule.object_id)
+ else: # All memories access
+ return None # All memories allowed
+ elif rule.effect == "deny":
+ if rule.object_id: # Specific memory denied
+ denied_memory_ids.add(rule.object_id)
+ else: # All memories denied
+ return set() # No memories accessible
+
+ # Remove denied memories from allowed set
+ if allowed_memory_ids:
+ allowed_memory_ids -= denied_memory_ids
+
+ return allowed_memory_ids
+
+
+# List all memories with filtering
+@router.get("/", response_model=Page[MemoryResponse])
+async def list_memories(
+ user_id: str,
+ app_id: Optional[UUID] = None,
+ from_date: Optional[int] = Query(
+ None,
+ description="Filter memories created after this date (timestamp)",
+ examples=[1718505600]
+ ),
+ to_date: Optional[int] = Query(
+ None,
+ description="Filter memories created before this date (timestamp)",
+ examples=[1718505600]
+ ),
+ categories: Optional[str] = None,
+ params: Params = Depends(),
+ search_query: Optional[str] = None,
+ sort_column: Optional[str] = Query(None, description="Column to sort by (memory, categories, app_name, created_at)"),
+ sort_direction: Optional[str] = Query(None, description="Sort direction (asc or desc)"),
+ db: Session = Depends(get_db)
+):
+ user = db.query(User).filter(User.user_id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ # Build base query
+ query = db.query(Memory).filter(
+ Memory.user_id == user.id,
+ Memory.state != MemoryState.deleted,
+ Memory.state != MemoryState.archived,
+ Memory.content.ilike(f"%{search_query}%") if search_query else True
+ )
+
+ # Apply filters
+ if app_id:
+ query = query.filter(Memory.app_id == app_id)
+
+ if from_date:
+ from_datetime = datetime.fromtimestamp(from_date, tz=UTC)
+ query = query.filter(Memory.created_at >= from_datetime)
+
+ if to_date:
+ to_datetime = datetime.fromtimestamp(to_date, tz=UTC)
+ query = query.filter(Memory.created_at <= to_datetime)
+
+ # Add joins for app and categories after filtering
+ query = query.outerjoin(App, Memory.app_id == App.id)
+ query = query.outerjoin(Memory.categories)
+
+ # Apply category filter if provided
+ if categories:
+ category_list = [c.strip() for c in categories.split(",")]
+ query = query.filter(Category.name.in_(category_list))
+
+ # Apply sorting if specified
+ if sort_column:
+ sort_field = getattr(Memory, sort_column, None)
+ if sort_field:
+ query = query.order_by(sort_field.desc()) if sort_direction == "desc" else query.order_by(sort_field.asc())
+
+
+ # Get paginated results
+ paginated_results = sqlalchemy_paginate(query, params)
+
+ # Filter results based on permissions
+ filtered_items = []
+ for item in paginated_results.items:
+ if check_memory_access_permissions(db, item, app_id):
+ filtered_items.append(item)
+
+ # Update paginated results with filtered items
+ paginated_results.items = filtered_items
+ paginated_results.total = len(filtered_items)
+
+ return paginated_results
+
+
+# Get all categories
+@router.get("/categories")
+async def get_categories(
+ user_id: str,
+ db: Session = Depends(get_db)
+):
+ user = db.query(User).filter(User.user_id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ # Get unique categories associated with the user's memories
+ # Get all memories
+ memories = db.query(Memory).filter(Memory.user_id == user.id, Memory.state != MemoryState.deleted, Memory.state != MemoryState.archived).all()
+ # Get all categories from memories
+ categories = [category for memory in memories for category in memory.categories]
+ # Get unique categories
+ unique_categories = list(set(categories))
+
+ return {
+ "categories": unique_categories,
+ "total": len(unique_categories)
+ }
+
+
+class CreateMemoryRequest(BaseModel):
+ user_id: str
+ text: str
+ metadata: dict = {}
+ infer: bool = True
+ app: str = "openmemory"
+
+
+# Create new memory
+@router.post("/")
+async def create_memory(
+ request: CreateMemoryRequest,
+ db: Session = Depends(get_db)
+):
+ user = db.query(User).filter(User.user_id == request.user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+ # Get or create app
+ app_obj = db.query(App).filter(App.name == request.app).first()
+ if not app_obj:
+ app_obj = App(name=request.app, owner_id=user.id)
+ db.add(app_obj)
+ db.commit()
+ db.refresh(app_obj)
+
+ # Check if app is active
+ if not app_obj.is_active:
+ raise HTTPException(status_code=403, detail=f"App {request.app} is currently paused on OpenMemory. Cannot create new memories.")
+
+ # Create memory
+ memory = Memory(
+ user_id=user.id,
+ app_id=app_obj.id,
+ content=request.text,
+ metadata_=request.metadata
+ )
+ db.add(memory)
+ db.commit()
+ db.refresh(memory)
+ return memory
+
+
+# Get memory by ID
+@router.get("/{memory_id}")
+async def get_memory(
+ memory_id: UUID,
+ db: Session = Depends(get_db)
+):
+ memory = get_memory_or_404(db, memory_id)
+ return {
+ "id": memory.id,
+ "text": memory.content,
+ "created_at": int(memory.created_at.timestamp()),
+ "state": memory.state.value,
+ "app_id": memory.app_id,
+ "app_name": memory.app.name if memory.app else None,
+ "categories": [category.name for category in memory.categories],
+ "metadata_": memory.metadata_
+ }
+
+
+class DeleteMemoriesRequest(BaseModel):
+ memory_ids: List[UUID]
+ user_id: str
+
+# Delete multiple memories
+@router.delete("/")
+async def delete_memories(
+ request: DeleteMemoriesRequest,
+ db: Session = Depends(get_db)
+):
+ user = db.query(User).filter(User.user_id == request.user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ for memory_id in request.memory_ids:
+ update_memory_state(db, memory_id, MemoryState.deleted, user.id)
+ return {"message": f"Successfully deleted {len(request.memory_ids)} memories"}
+
+
+# Archive memories
+@router.post("/actions/archive")
+async def archive_memories(
+ memory_ids: List[UUID],
+ user_id: UUID,
+ db: Session = Depends(get_db)
+):
+ for memory_id in memory_ids:
+ update_memory_state(db, memory_id, MemoryState.archived, user_id)
+ return {"message": f"Successfully archived {len(memory_ids)} memories"}
+
+
+class PauseMemoriesRequest(BaseModel):
+ memory_ids: Optional[List[UUID]] = None
+ category_ids: Optional[List[UUID]] = None
+ app_id: Optional[UUID] = None
+ all_for_app: bool = False
+ global_pause: bool = False
+ state: Optional[MemoryState] = None
+ user_id: str
+
+# Pause access to memories
+@router.post("/actions/pause")
+async def pause_memories(
+ request: PauseMemoriesRequest,
+ db: Session = Depends(get_db)
+):
+
+ global_pause = request.global_pause
+ all_for_app = request.all_for_app
+ app_id = request.app_id
+ memory_ids = request.memory_ids
+ category_ids = request.category_ids
+ state = request.state or MemoryState.paused
+
+ user = db.query(User).filter(User.user_id == request.user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ user_id = user.id
+
+ if global_pause:
+ # Pause all memories
+ memories = db.query(Memory).filter(
+ Memory.state != MemoryState.deleted,
+ Memory.state != MemoryState.archived
+ ).all()
+ for memory in memories:
+ update_memory_state(db, memory.id, state, user_id)
+ return {"message": "Successfully paused all memories"}
+
+ if app_id:
+ # Pause all memories for an app
+ memories = db.query(Memory).filter(
+ Memory.app_id == app_id,
+ Memory.user_id == user.id,
+ Memory.state != MemoryState.deleted,
+ Memory.state != MemoryState.archived
+ ).all()
+ for memory in memories:
+ update_memory_state(db, memory.id, state, user_id)
+ return {"message": f"Successfully paused all memories for app {app_id}"}
+
+ if all_for_app and memory_ids:
+ # Pause all memories for an app
+ memories = db.query(Memory).filter(
+ Memory.user_id == user.id,
+ Memory.state != MemoryState.deleted,
+ Memory.id.in_(memory_ids)
+ ).all()
+ for memory in memories:
+ update_memory_state(db, memory.id, state, user_id)
+ return {"message": f"Successfully paused all memories"}
+
+ if memory_ids:
+ # Pause specific memories
+ for memory_id in memory_ids:
+ update_memory_state(db, memory_id, state, user_id)
+ return {"message": f"Successfully paused {len(memory_ids)} memories"}
+
+ if category_ids:
+ # Pause memories by category
+ memories = db.query(Memory).join(Memory.categories).filter(
+ Category.id.in_(category_ids),
+ Memory.state != MemoryState.deleted,
+ Memory.state != MemoryState.archived
+ ).all()
+ for memory in memories:
+ update_memory_state(db, memory.id, state, user_id)
+ return {"message": f"Successfully paused memories in {len(category_ids)} categories"}
+
+ raise HTTPException(status_code=400, detail="Invalid pause request parameters")
+
+
+# Get memory access logs
+@router.get("/{memory_id}/access-log")
+async def get_memory_access_log(
+ memory_id: UUID,
+ page: int = Query(1, ge=1),
+ page_size: int = Query(10, ge=1, le=100),
+ db: Session = Depends(get_db)
+):
+ query = db.query(MemoryAccessLog).filter(MemoryAccessLog.memory_id == memory_id)
+ total = query.count()
+ logs = query.order_by(MemoryAccessLog.accessed_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
+
+ # Get app name
+ for log in logs:
+ app = db.query(App).filter(App.id == log.app_id).first()
+ log.app_name = app.name if app else None
+
+ return {
+ "total": total,
+ "page": page,
+ "page_size": page_size,
+ "logs": logs
+ }
+
+
+class UpdateMemoryRequest(BaseModel):
+ memory_content: str
+ user_id: str
+
+# Update a memory
+@router.put("/{memory_id}")
+async def update_memory(
+ memory_id: UUID,
+ request: UpdateMemoryRequest,
+ db: Session = Depends(get_db)
+):
+ user = db.query(User).filter(User.user_id == request.user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+ memory = get_memory_or_404(db, memory_id)
+ memory.content = request.memory_content
+ db.commit()
+ db.refresh(memory)
+ return memory
+
+class FilterMemoriesRequest(BaseModel):
+ user_id: str
+ page: int = 1
+ size: int = 10
+ search_query: Optional[str] = None
+ app_ids: Optional[List[UUID]] = None
+ category_ids: Optional[List[UUID]] = None
+ sort_column: Optional[str] = None
+ sort_direction: Optional[str] = None
+ from_date: Optional[int] = None
+ to_date: Optional[int] = None
+ show_archived: Optional[bool] = False
+
+@router.post("/filter", response_model=Page[MemoryResponse])
+async def filter_memories(
+ request: FilterMemoriesRequest,
+ db: Session = Depends(get_db)
+):
+ user = db.query(User).filter(User.user_id == request.user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ # Build base query
+ query = db.query(Memory).filter(
+ Memory.user_id == user.id,
+ Memory.state != MemoryState.deleted,
+ )
+
+ # Filter archived memories based on show_archived parameter
+ if not request.show_archived:
+ query = query.filter(Memory.state != MemoryState.archived)
+
+ # Apply search filter
+ if request.search_query:
+ query = query.filter(Memory.content.ilike(f"%{request.search_query}%"))
+
+ # Apply app filter
+ if request.app_ids:
+ query = query.filter(Memory.app_id.in_(request.app_ids))
+
+ # Add joins for app and categories
+ query = query.outerjoin(App, Memory.app_id == App.id)
+
+ # Apply category filter
+ if request.category_ids:
+ query = query.join(Memory.categories).filter(Category.id.in_(request.category_ids))
+ else:
+ query = query.outerjoin(Memory.categories)
+
+ # Apply date filters
+ if request.from_date:
+ from_datetime = datetime.fromtimestamp(request.from_date, tz=UTC)
+ query = query.filter(Memory.created_at >= from_datetime)
+
+ if request.to_date:
+ to_datetime = datetime.fromtimestamp(request.to_date, tz=UTC)
+ query = query.filter(Memory.created_at <= to_datetime)
+
+ # Apply sorting
+ if request.sort_column and request.sort_direction:
+ sort_direction = request.sort_direction.lower()
+ if sort_direction not in ['asc', 'desc']:
+ raise HTTPException(status_code=400, detail="Invalid sort direction")
+
+ sort_mapping = {
+ 'memory': Memory.content,
+ 'app_name': App.name,
+ 'created_at': Memory.created_at
+ }
+
+ if request.sort_column not in sort_mapping:
+ raise HTTPException(status_code=400, detail="Invalid sort column")
+
+ sort_field = sort_mapping[request.sort_column]
+ if sort_direction == 'desc':
+ query = query.order_by(sort_field.desc())
+ else:
+ query = query.order_by(sort_field.asc())
+ else:
+ # Default sorting
+ query = query.order_by(Memory.created_at.desc())
+
+ # Add eager loading for categories and make the query distinct
+ query = query.options(
+ joinedload(Memory.categories)
+ ).distinct(Memory.id)
+
+ # Use fastapi-pagination's paginate function
+ return sqlalchemy_paginate(
+ query,
+ Params(page=request.page, size=request.size),
+ transformer=lambda items: [
+ MemoryResponse(
+ id=memory.id,
+ content=memory.content,
+ created_at=memory.created_at,
+ state=memory.state.value,
+ app_id=memory.app_id,
+ app_name=memory.app.name if memory.app else None,
+ categories=[category.name for category in memory.categories],
+ metadata_=memory.metadata_
+ )
+ for memory in items
+ ]
+ )
+
+
+@router.get("/{memory_id}/related", response_model=Page[MemoryResponse])
+async def get_related_memories(
+ memory_id: UUID,
+ user_id: str,
+ params: Params = Depends(),
+ db: Session = Depends(get_db)
+):
+ # Validate user
+ user = db.query(User).filter(User.user_id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ # Get the source memory
+ memory = get_memory_or_404(db, memory_id)
+
+ # Extract category IDs from the source memory
+ category_ids = [category.id for category in memory.categories]
+
+ if not category_ids:
+ return Page.create([], total=0, params=params)
+
+ # Build query for related memories
+ query = db.query(Memory).distinct(Memory.id).filter(
+ Memory.user_id == user.id,
+ Memory.id != memory_id,
+ Memory.state != MemoryState.deleted
+ ).join(Memory.categories).filter(
+ Category.id.in_(category_ids)
+ ).options(
+ joinedload(Memory.categories),
+ joinedload(Memory.app)
+ ).order_by(
+ func.count(Category.id).desc(),
+ Memory.created_at.desc()
+ ).group_by(Memory.id)
+
+ # ⚡ Force page size to be 5
+ params = Params(page=params.page, size=5)
+
+ return sqlalchemy_paginate(
+ query,
+ params,
+ transformer=lambda items: [
+ MemoryResponse(
+ id=memory.id,
+ content=memory.content,
+ created_at=memory.created_at,
+ state=memory.state.value,
+ app_id=memory.app_id,
+ app_name=memory.app.name if memory.app else None,
+ categories=[category.name for category in memory.categories],
+ metadata_=memory.metadata_
+ )
+ for memory in items
+ ]
+ )
diff --git a/openmemory/api/app/routers/stats.py b/openmemory/api/app/routers/stats.py
new file mode 100644
index 00000000..047721f1
--- /dev/null
+++ b/openmemory/api/app/routers/stats.py
@@ -0,0 +1,30 @@
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+from app.database import get_db
+from app.models import User, Memory, App, MemoryState
+
+
+router = APIRouter(prefix="/api/v1/stats", tags=["stats"])
+
+@router.get("/")
+async def get_profile(
+ user_id: str,
+ db: Session = Depends(get_db)
+):
+ user = db.query(User).filter(User.user_id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ # Get total number of memories
+ total_memories = db.query(Memory).filter(Memory.user_id == user.id, Memory.state != MemoryState.deleted).count()
+
+ # Get total number of apps
+ apps = db.query(App).filter(App.owner == user)
+ total_apps = apps.count()
+
+ return {
+ "total_memories": total_memories,
+ "total_apps": total_apps,
+ "apps": apps.all()
+ }
+
diff --git a/openmemory/api/app/schemas.py b/openmemory/api/app/schemas.py
new file mode 100644
index 00000000..35a512f6
--- /dev/null
+++ b/openmemory/api/app/schemas.py
@@ -0,0 +1,64 @@
+from datetime import datetime
+from typing import Optional, List
+from uuid import UUID
+from pydantic import BaseModel, Field, validator
+
+class MemoryBase(BaseModel):
+ content: str
+ metadata_: Optional[dict] = Field(default_factory=dict)
+
+class MemoryCreate(MemoryBase):
+ user_id: UUID
+ app_id: UUID
+
+
+class Category(BaseModel):
+ name: str
+
+
+class App(BaseModel):
+ id: UUID
+ name: str
+
+
+class Memory(MemoryBase):
+ id: UUID
+ user_id: UUID
+ app_id: UUID
+ created_at: datetime
+ updated_at: Optional[datetime] = None
+ state: str
+ categories: Optional[List[Category]] = None
+ app: App
+
+ class Config:
+ from_attributes = True
+
+class MemoryUpdate(BaseModel):
+ content: Optional[str] = None
+ metadata_: Optional[dict] = None
+ state: Optional[str] = None
+
+
+class MemoryResponse(BaseModel):
+ id: UUID
+ content: str
+ created_at: int
+ state: str
+ app_id: UUID
+ app_name: str
+ categories: List[str]
+ metadata_: Optional[dict] = None
+
+ @validator('created_at', pre=True)
+ def convert_to_epoch(cls, v):
+ if isinstance(v, datetime):
+ return int(v.timestamp())
+ return v
+
+class PaginatedMemoryResponse(BaseModel):
+ items: List[MemoryResponse]
+ total: int
+ page: int
+ size: int
+ pages: int
diff --git a/openmemory/api/app/utils/__init__.py b/openmemory/api/app/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/openmemory/api/app/utils/categorization.py b/openmemory/api/app/utils/categorization.py
new file mode 100644
index 00000000..bcfbe657
--- /dev/null
+++ b/openmemory/api/app/utils/categorization.py
@@ -0,0 +1,37 @@
+import json
+import logging
+
+from openai import OpenAI
+from typing import List
+from dotenv import load_dotenv
+from pydantic import BaseModel
+from tenacity import retry, stop_after_attempt, wait_exponential
+from app.utils.prompts import MEMORY_CATEGORIZATION_PROMPT
+
+load_dotenv()
+
+openai_client = OpenAI()
+
+
+class MemoryCategories(BaseModel):
+ categories: List[str]
+
+
+@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15))
+def get_categories_for_memory(memory: str) -> List[str]:
+ """Get categories for a memory."""
+ try:
+ response = openai_client.responses.parse(
+ model="gpt-4o-mini",
+ instructions=MEMORY_CATEGORIZATION_PROMPT,
+ input=memory,
+ temperature=0,
+ text_format=MemoryCategories,
+ )
+ response_json =json.loads(response.output[0].content[0].text)
+ categories = response_json['categories']
+ categories = [cat.strip().lower() for cat in categories]
+ # TODO: Validate categories later may be
+ return categories
+ except Exception as e:
+ raise e
diff --git a/openmemory/api/app/utils/db.py b/openmemory/api/app/utils/db.py
new file mode 100644
index 00000000..bf2fcf9c
--- /dev/null
+++ b/openmemory/api/app/utils/db.py
@@ -0,0 +1,32 @@
+from sqlalchemy.orm import Session
+from app.models import User, App
+from typing import Tuple
+
+
+def get_or_create_user(db: Session, user_id: str) -> User:
+ """Get or create a user with the given user_id"""
+ user = db.query(User).filter(User.user_id == user_id).first()
+ if not user:
+ user = User(user_id=user_id)
+ db.add(user)
+ db.commit()
+ db.refresh(user)
+ return user
+
+
+def get_or_create_app(db: Session, user: User, app_id: str) -> App:
+ """Get or create an app for the given user"""
+ app = db.query(App).filter(App.owner_id == user.id, App.name == app_id).first()
+ if not app:
+ app = App(owner_id=user.id, name=app_id)
+ db.add(app)
+ db.commit()
+ db.refresh(app)
+ return app
+
+
+def get_user_and_app(db: Session, user_id: str, app_id: str) -> Tuple[User, App]:
+ """Get or create both user and their app"""
+ user = get_or_create_user(db, user_id)
+ app = get_or_create_app(db, user, app_id)
+ return user, app
diff --git a/openmemory/api/app/utils/memory.py b/openmemory/api/app/utils/memory.py
new file mode 100644
index 00000000..5cacc3c4
--- /dev/null
+++ b/openmemory/api/app/utils/memory.py
@@ -0,0 +1,51 @@
+import os
+
+from mem0 import Memory
+
+
+memory_client = None
+
+
+def get_memory_client(custom_instructions: str = None):
+ """
+ Get or initialize the Mem0 client.
+
+ Args:
+ custom_instructions: Optional instructions for the memory project.
+
+ Returns:
+ Initialized Mem0 client instance.
+
+ Raises:
+ Exception: If required API keys are not set.
+ """
+ global memory_client
+
+ if memory_client is not None:
+ return memory_client
+
+ try:
+ config = {
+ "vector_store": {
+ "provider": "qdrant",
+ "config": {
+ "collection_name": "openmemory",
+ "host": "mem0_store",
+ "port": 6333,
+ }
+ }
+ }
+
+ memory_client = Memory.from_config(config_dict=config)
+ except Exception:
+ raise Exception("Exception occurred while initializing memory client")
+
+ # Update project with custom instructions if provided
+ if custom_instructions:
+ memory_client.update_project(custom_instructions=custom_instructions)
+
+ return memory_client
+
+
+def get_default_user_id():
+ return "default_user"
diff --git a/openmemory/api/app/utils/permissions.py b/openmemory/api/app/utils/permissions.py
new file mode 100644
index 00000000..4b8a04ed
--- /dev/null
+++ b/openmemory/api/app/utils/permissions.py
@@ -0,0 +1,52 @@
+from typing import Optional
+from uuid import UUID
+from sqlalchemy.orm import Session
+from app.models import Memory, App, MemoryState
+
+
+def check_memory_access_permissions(
+ db: Session,
+ memory: Memory,
+ app_id: Optional[UUID] = None
+) -> bool:
+ """
+ Check if the given app has permission to access a memory based on:
+ 1. Memory state (must be active)
+ 2. App state (must not be paused)
+ 3. App-specific access controls
+
+ Args:
+ db: Database session
+ memory: Memory object to check access for
+ app_id: Optional app ID to check permissions for
+
+ Returns:
+ bool: True if access is allowed, False otherwise
+ """
+ # Check if memory is active
+ if memory.state != MemoryState.active:
+ return False
+
+ # If no app_id provided, only check memory state
+ if not app_id:
+ return True
+
+ # Check if app exists and is active
+ app = db.query(App).filter(App.id == app_id).first()
+ if not app:
+ return False
+
+ # Check if app is paused/inactive
+ if not app.is_active:
+ return False
+
+ # Check app-specific access controls
+ from app.routers.memories import get_accessible_memory_ids
+ accessible_memory_ids = get_accessible_memory_ids(db, app_id)
+
+ # If accessible_memory_ids is None, all memories are accessible
+ if accessible_memory_ids is None:
+ return True
+
+ # Check if memory is in the accessible set
+ return memory.id in accessible_memory_ids
diff --git a/openmemory/api/app/utils/prompts.py b/openmemory/api/app/utils/prompts.py
new file mode 100644
index 00000000..669f2e60
--- /dev/null
+++ b/openmemory/api/app/utils/prompts.py
@@ -0,0 +1,28 @@
+MEMORY_CATEGORIZATION_PROMPT = """Your task is to assign each piece of information (or “memory”) to one or more of the following categories. Feel free to use multiple categories per item when appropriate.
+
+- Personal: family, friends, home, hobbies, lifestyle
+- Relationships: social network, significant others, colleagues
+- Preferences: likes, dislikes, habits, favorite media
+- Health: physical fitness, mental health, diet, sleep
+- Travel: trips, commutes, favorite places, itineraries
+- Work: job roles, companies, projects, promotions
+- Education: courses, degrees, certifications, skills development
+- Projects: to‑dos, milestones, deadlines, status updates
+- AI, ML & Technology: infrastructure, algorithms, tools, research
+- Technical Support: bug reports, error logs, fixes
+- Finance: income, expenses, investments, billing
+- Shopping: purchases, wishlists, returns, deliveries
+- Legal: contracts, policies, regulations, privacy
+- Entertainment: movies, music, games, books, events
+- Messages: emails, SMS, alerts, reminders
+- Customer Support: tickets, inquiries, resolutions
+- Product Feedback: ratings, bug reports, feature requests
+- News: articles, headlines, trending topics
+- Organization: meetings, appointments, calendars
+- Goals: ambitions, KPIs, long‑term objectives
+
+Guidelines:
+- Return only the categories under 'categories' key in the JSON format.
+- If you cannot categorize the memory, return an empty list with key 'categories'.
+- Don't limit yourself to the categories listed above only. Feel free to create new categories based on the memory. Make sure that it is a single phrase.
+"""
diff --git a/openmemory/api/docker-compose.yml b/openmemory/api/docker-compose.yml
new file mode 100644
index 00000000..91434645
--- /dev/null
+++ b/openmemory/api/docker-compose.yml
@@ -0,0 +1,26 @@
+services:
+ mem0_store:
+ image: qdrant/qdrant
+ ports:
+ - "6333:6333"
+ volumes:
+ - mem0_storage:/mem0/storage
+ api:
+ image: mem0/openmemory-mcp
+ build: .
+ environment:
+ - OPENAI_API_KEY
+ - USER
+ env_file:
+ - .env
+ depends_on:
+ - mem0_store
+ ports:
+ - "8765:8765"
+ volumes:
+ - .:/usr/src/openmemory
+ command: >
+ sh -c "uvicorn main:app --host 0.0.0.0 --port 8765 --reload --workers 4"
+
+volumes:
+ mem0_storage:
diff --git a/openmemory/api/main.py b/openmemory/api/main.py
new file mode 100644
index 00000000..a519dd84
--- /dev/null
+++ b/openmemory/api/main.py
@@ -0,0 +1,86 @@
+import datetime
+from fastapi import FastAPI
+from app.database import engine, Base, SessionLocal
+from app.mcp_server import setup_mcp_server
+from app.routers import memories_router, apps_router, stats_router
+from fastapi_pagination import add_pagination
+from fastapi.middleware.cors import CORSMiddleware
+from app.models import User, App
+from uuid import uuid4
+from app.config import USER_ID, DEFAULT_APP_ID
+
+app = FastAPI(title="OpenMemory API")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Create all tables
+Base.metadata.create_all(bind=engine)
+
+# Check for USER_ID and create default user if needed
+def create_default_user():
+ db = SessionLocal()
+ try:
+ # Check if user exists
+ user = db.query(User).filter(User.user_id == USER_ID).first()
+ if not user:
+ # Create default user
+ user = User(
+ id=uuid4(),
+ user_id=USER_ID,
+ name="Default User",
+ created_at=datetime.datetime.now(datetime.UTC)
+ )
+ db.add(user)
+ db.commit()
+ finally:
+ db.close()
+
+
+def create_default_app():
+ db = SessionLocal()
+ try:
+ user = db.query(User).filter(User.user_id == USER_ID).first()
+ if not user:
+ return
+
+ # Check if app already exists
+ existing_app = db.query(App).filter(
+ App.name == DEFAULT_APP_ID,
+ App.owner_id == user.id
+ ).first()
+
+ if existing_app:
+ return
+
+ app = App(
+ id=uuid4(),
+ name=DEFAULT_APP_ID,
+ owner_id=user.id,
+ created_at=datetime.datetime.now(datetime.UTC),
+ updated_at=datetime.datetime.now(datetime.UTC),
+ )
+ db.add(app)
+ db.commit()
+ finally:
+ db.close()
+
+# Create default user on startup
+create_default_user()
+create_default_app()
+
+# Setup MCP server
+setup_mcp_server(app)
+
+# Include routers
+app.include_router(memories_router)
+app.include_router(apps_router)
+app.include_router(stats_router)
+
+# Add pagination support
+add_pagination(app)
diff --git a/openmemory/api/requirements.txt b/openmemory/api/requirements.txt
new file mode 100644
index 00000000..9b973de5
--- /dev/null
+++ b/openmemory/api/requirements.txt
@@ -0,0 +1,15 @@
+fastapi>=0.68.0
+uvicorn>=0.15.0
+sqlalchemy>=1.4.0
+python-dotenv>=0.19.0
+alembic>=1.7.0
+psycopg2-binary>=2.9.0
+python-multipart>=0.0.5
+fastapi-pagination>=0.12.0
+mem0ai>=0.1.92
+mcp[cli]>=1.3.0
+pytest>=7.0.0
+pytest-asyncio>=0.21.0
+httpx>=0.24.0
+pytest-cov>=4.0.0
+tenacity==9.1.2
diff --git a/openmemory/ui/.env.dev b/openmemory/ui/.env.dev
new file mode 100644
index 00000000..896ce957
--- /dev/null
+++ b/openmemory/ui/.env.dev
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_API_URL=NEXT_PUBLIC_API_URL
+NEXT_PUBLIC_USER_ID=NEXT_PUBLIC_USER_ID
\ No newline at end of file
diff --git a/openmemory/ui/.env.example b/openmemory/ui/.env.example
new file mode 100644
index 00000000..1a8182f8
--- /dev/null
+++ b/openmemory/ui/.env.example
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_API_URL=http://localhost:8000
+NEXT_PUBLIC_USER_ID=default-user
diff --git a/openmemory/ui/Dockerfile b/openmemory/ui/Dockerfile
new file mode 100644
index 00000000..ec0f7f8d
--- /dev/null
+++ b/openmemory/ui/Dockerfile
@@ -0,0 +1,62 @@
+# syntax=docker.io/docker/dockerfile:1
+
+# Base stage for common setup
+FROM node:18-alpine AS base
+
+# Install dependencies for pnpm
+RUN apk add --no-cache libc6-compat curl && \
+ corepack enable && \
+ corepack prepare pnpm@latest --activate
+
+WORKDIR /app
+
+# Dependencies stage
+FROM base AS deps
+
+# Copy lockfile and manifest
+COPY package.json pnpm-lock.yaml ./
+
+# Install dependencies using pnpm
+RUN pnpm install --frozen-lockfile
+
+# Builder stage
+FROM base AS builder
+WORKDIR /app
+
+COPY --from=deps /app/node_modules ./node_modules
+COPY --from=deps /app/pnpm-lock.yaml ./pnpm-lock.yaml
+COPY . .
+
+RUN cp next.config.dev.mjs next.config.mjs
+RUN cp .env.dev .env
+
+RUN pnpm build
+
+# Production runner stage
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV=production
+
+# Create non-root user for security
+RUN addgroup --system --gid 1001 nodejs && \
+ adduser --system --uid 1001 nextjs
+
+# Copy production dependencies and built artifacts
+COPY --from=builder /app/public ./public
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+# Copy and prepare entrypoint script
+COPY --chown=nextjs:nodejs entrypoint.sh /home/nextjs/entrypoint.sh
+RUN chmod +x /home/nextjs/entrypoint.sh
+
+# Switch to non-root user
+USER nextjs
+
+EXPOSE 3000
+ENV PORT=3000
+ENV HOSTNAME="0.0.0.0"
+
+ENTRYPOINT ["/home/nextjs/entrypoint.sh"]
+CMD ["node", "server.js"]
diff --git a/openmemory/ui/app/apps/[appId]/components/AppDetailCard.tsx b/openmemory/ui/app/apps/[appId]/components/AppDetailCard.tsx
new file mode 100644
index 00000000..7f3f3c4f
--- /dev/null
+++ b/openmemory/ui/app/apps/[appId]/components/AppDetailCard.tsx
@@ -0,0 +1,166 @@
+import React, { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { PauseIcon, Loader2, PlayIcon } from "lucide-react";
+import { useAppsApi } from "@/hooks/useAppsApi";
+import Image from "next/image";
+import { useDispatch, useSelector } from "react-redux";
+import { setAppDetails } from "@/store/appsSlice";
+import { BiEdit } from "react-icons/bi";
+import { constants } from "@/components/shared/source-app";
+import { RootState } from "@/store/store";
+
+const capitalize = (str: string) => {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+};
+
+const AppDetailCard = ({
+ appId,
+ selectedApp,
+}: {
+ appId: string;
+ selectedApp: any;
+}) => {
+ const { updateAppDetails } = useAppsApi();
+ const [isLoading, setIsLoading] = useState(false);
+ const dispatch = useDispatch();
+ const apps = useSelector((state: RootState) => state.apps.apps);
+ const currentApp = apps.find((app: any) => app.id === appId);
+ const appConfig = currentApp
+ ? constants[currentApp.name as keyof typeof constants] || constants.default
+ : constants.default;
+
+ const handlePauseAccess = async () => {
+ setIsLoading(true);
+ try {
+ await updateAppDetails(appId, {
+ is_active: !selectedApp.details.is_active,
+ });
+ dispatch(
+ setAppDetails({ appId, isActive: !selectedApp.details.is_active })
+ );
+ } catch (error) {
+ console.error("Failed to toggle app pause state:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const buttonText = selectedApp.details.is_active
+ ? "Pause Access"
+ : "Unpause Access";
+
+ return (
+
+
+
+
+ {appConfig.iconImage ? (
+
+ ) : (
+
+
+
+ )}
+
+
{appConfig.name}
+
+
+
+
+
Access Status
+
+ {capitalize(
+ selectedApp.details.is_active ? "active" : "inactive"
+ )}
+
+
+
+
+
Total Memories Created
+
+ {selectedApp.details.total_memories_created} Memories
+
+
+
+
+
Total Memories Accessed
+
+ {selectedApp.details.total_memories_accessed} Memories
+
+
+
+
+
First Accessed
+
+ {selectedApp.details.first_accessed
+ ? new Date(
+ selectedApp.details.first_accessed
+ ).toLocaleDateString("en-US", {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })
+ : "Never"}
+
+
+
+
+
Last Accessed
+
+ {selectedApp.details.last_accessed
+ ? new Date(
+ selectedApp.details.last_accessed
+ ).toLocaleDateString("en-US", {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })
+ : "Never"}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AppDetailCard;
diff --git a/openmemory/ui/app/apps/[appId]/components/MemoryCard.tsx b/openmemory/ui/app/apps/[appId]/components/MemoryCard.tsx
new file mode 100644
index 00000000..dfb35cb1
--- /dev/null
+++ b/openmemory/ui/app/apps/[appId]/components/MemoryCard.tsx
@@ -0,0 +1,115 @@
+import { ArrowRight } from "lucide-react";
+import Categories from "@/components/shared/categories";
+import Link from "next/link";
+import { constants } from "@/components/shared/source-app";
+import Image from "next/image";
+interface MemoryCardProps {
+ id: string;
+ content: string;
+ created_at: string;
+ metadata?: Record;
+ categories?: string[];
+ access_count?: number;
+ app_name: string;
+ state: string;
+}
+
+export function MemoryCard({
+ id,
+ content,
+ created_at,
+ metadata,
+ categories,
+ access_count,
+ app_name,
+ state,
+}: MemoryCardProps) {
+ return (
+
+
+
+
+ {metadata && Object.keys(metadata).length > 0 && (
+
+
METADATA
+
+
+ {JSON.stringify(metadata, null, 2)}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {access_count ? (
+
+ Accessed {access_count} times
+
+ ) : (
+ new Date(created_at + "Z").toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })
+ )}
+
+
+ {state !== "active" && (
+
+ {state === "paused" ? "Paused" : "Archived"}
+
+ )}
+
+
+ {!app_name && (
+
+ View Details
+
+
+ )}
+ {app_name && (
+
+
+
Created by:
+
+
+
+
+ {constants[app_name as keyof typeof constants]?.name}
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/apps/[appId]/page.tsx b/openmemory/ui/app/apps/[appId]/page.tsx
new file mode 100644
index 00000000..ecd90313
--- /dev/null
+++ b/openmemory/ui/app/apps/[appId]/page.tsx
@@ -0,0 +1,219 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useParams } from "next/navigation";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store/store";
+import { useAppsApi } from "@/hooks/useAppsApi";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { MemoryCard } from "./components/MemoryCard";
+import AppDetailCard from "./components/AppDetailCard";
+import "@/styles/animation.css";
+import NotFound from "@/app/not-found";
+import { AppDetailCardSkeleton } from "@/skeleton/AppDetailCardSkeleton";
+import { MemoryCardSkeleton } from "@/skeleton/MemoryCardSkeleton";
+
+export default function AppDetailsPage() {
+ const params = useParams();
+ const appId = params.appId as string;
+ const [activeTab, setActiveTab] = useState("created");
+
+ const {
+ fetchAppDetails,
+ fetchAppMemories,
+ fetchAppAccessedMemories,
+ fetchApps,
+ } = useAppsApi();
+ const selectedApp = useSelector((state: RootState) => state.apps.selectedApp);
+
+ useEffect(() => {
+ fetchApps({});
+ }, [fetchApps]);
+
+ useEffect(() => {
+ const loadData = async () => {
+ if (appId) {
+ try {
+ // Load all data in parallel
+ await Promise.all([
+ fetchAppDetails(appId),
+ fetchAppMemories(appId),
+ fetchAppAccessedMemories(appId),
+ ]);
+ } catch (error) {
+ console.error("Error loading app data:", error);
+ }
+ }
+ };
+
+ loadData();
+ }, [appId, fetchAppDetails, fetchAppMemories, fetchAppAccessedMemories]);
+
+ if (selectedApp.error) {
+ return (
+
+ );
+ }
+
+ if (!selectedApp.details) {
+ return (
+
+
+
+
+
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+ );
+ }
+
+ const renderCreatedMemories = () => {
+ const memories = selectedApp.memories.created;
+
+ if (memories.loading) {
+ return (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ if (memories.error) {
+ return (
+
+ );
+ }
+
+ if (memories.items.length === 0) {
+ return (
+ No memories found
+ );
+ }
+
+ return memories.items.map((memory) => (
+
+ ));
+ };
+
+ const renderAccessedMemories = () => {
+ const memories = selectedApp.memories.accessed;
+
+ if (memories.loading) {
+ return (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ if (memories.error) {
+ return (
+
+ Error loading memories: {memories.error}
+
+ );
+ }
+
+ if (memories.items.length === 0) {
+ return (
+
+ No accessed memories found
+
+ );
+ }
+
+ return memories.items.map((accessedMemory) => (
+
+
+
+ ));
+ };
+
+ return (
+
+
+ {/* Main content area */}
+
+
+
+
+ Created ({selectedApp.memories.created.total})
+
+
+ Accessed ({selectedApp.memories.accessed.total})
+
+
+
+
+ {renderCreatedMemories()}
+
+
+
+ {renderAccessedMemories()}
+
+
+
+
+ {/* Sidebar */}
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/apps/components/AppCard.tsx b/openmemory/ui/app/apps/components/AppCard.tsx
new file mode 100644
index 00000000..13a4fe0c
--- /dev/null
+++ b/openmemory/ui/app/apps/components/AppCard.tsx
@@ -0,0 +1,84 @@
+import type React from "react";
+import { ArrowRight } from "lucide-react";
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+} from "@/components/ui/card";
+
+import { constants } from "@/components/shared/source-app";
+import { App } from "@/store/appsSlice";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+
+interface AppCardProps {
+ app: App;
+}
+
+export function AppCard({ app }: AppCardProps) {
+ const router = useRouter();
+ const appConfig =
+ constants[app.name as keyof typeof constants] || constants.default;
+ const isActive = app.is_active;
+
+ return (
+
+
+
+
+ {appConfig.iconImage ? (
+
+
+
+ ) : (
+
+ {appConfig.icon}
+
+ )}
+
+
{appConfig.name}
+
+
+
+
+
+
Memories Created
+
+ {app.total_memories_created.toLocaleString()} Memories
+
+
+
+
Memories Accessed
+
+ {app.total_memories_accessed.toLocaleString()} Memories
+
+
+
+
+
+
+
+ {isActive ? "Active" : "Inactive"}
+
+ router.push(`/apps/${app.id}`)}
+ className="border hover:cursor-pointer border-zinc-700 bg-zinc-950 flex items-center px-3 py-1 text-sm rounded-lg text-white p-0 hover:bg-zinc-950/50 hover:text-white"
+ >
+ View Details
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/apps/components/AppFilters.tsx b/openmemory/ui/app/apps/components/AppFilters.tsx
new file mode 100644
index 00000000..25aad06d
--- /dev/null
+++ b/openmemory/ui/app/apps/components/AppFilters.tsx
@@ -0,0 +1,150 @@
+"use client";
+import { useEffect, useState } from "react";
+import { Search, ChevronDown, SortAsc, SortDesc } from "lucide-react";
+import { useDispatch, useSelector } from "react-redux";
+import {
+ setSearchQuery,
+ setActiveFilter,
+ setSortBy,
+ setSortDirection,
+} from "@/store/appsSlice";
+import { RootState } from "@/store/store";
+import { useCallback } from "react";
+import debounce from "lodash/debounce";
+import { useAppsApi } from "@/hooks/useAppsApi";
+import { AppFiltersSkeleton } from "@/skeleton/AppFiltersSkeleton";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Input } from "@/components/ui/input";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuGroup,
+} from "@/components/ui/dropdown-menu";
+import { Button } from "@/components/ui/button";
+
+const sortOptions = [
+ { value: "name", label: "Name" },
+ { value: "memories", label: "Memories Created" },
+ { value: "memories_accessed", label: "Memories Accessed" },
+];
+
+export function AppFilters() {
+ const dispatch = useDispatch();
+ const filters = useSelector((state: RootState) => state.apps.filters);
+ const [localSearch, setLocalSearch] = useState(filters.searchQuery);
+ const { isLoading } = useAppsApi();
+
+ const debouncedSearch = useCallback(
+ debounce((query: string) => {
+ dispatch(setSearchQuery(query));
+ }, 300),
+ [dispatch]
+ );
+
+ const handleSearchChange = (e: React.ChangeEvent) => {
+ const query = e.target.value;
+ setLocalSearch(query);
+ debouncedSearch(query);
+ };
+
+ const handleActiveFilterChange = (value: string) => {
+ dispatch(setActiveFilter(value === "all" ? "all" : value === "true"));
+ };
+
+ const setSorting = (sortBy: "name" | "memories" | "memories_accessed") => {
+ const newDirection =
+ filters.sortBy === sortBy && filters.sortDirection === "asc"
+ ? "desc"
+ : "asc";
+ dispatch(setSortBy(sortBy));
+ dispatch(setSortDirection(newDirection));
+ };
+
+ useEffect(() => {
+ setLocalSearch(filters.searchQuery);
+ }, [filters.searchQuery]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sort by
+
+
+ {sortOptions.map((option) => (
+
+ setSorting(
+ option.value as "name" | "memories" | "memories_accessed"
+ )
+ }
+ className="cursor-pointer flex justify-between items-center"
+ >
+ {option.label}
+ {filters.sortBy === option.value &&
+ (filters.sortDirection === "asc" ? (
+
+ ) : (
+
+ ))}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/apps/components/AppGrid.tsx b/openmemory/ui/app/apps/components/AppGrid.tsx
new file mode 100644
index 00000000..b075e120
--- /dev/null
+++ b/openmemory/ui/app/apps/components/AppGrid.tsx
@@ -0,0 +1,48 @@
+"use client";
+import { useEffect } from "react";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store/store";
+import { useAppsApi } from "@/hooks/useAppsApi";
+import { AppCard } from "./AppCard";
+import { AppCardSkeleton } from "@/skeleton/AppCardSkeleton";
+
+export function AppGrid() {
+ const { fetchApps, isLoading } = useAppsApi();
+ const apps = useSelector((state: RootState) => state.apps.apps);
+ const filters = useSelector((state: RootState) => state.apps.filters);
+
+ useEffect(() => {
+ fetchApps({
+ name: filters.searchQuery,
+ is_active: filters.isActive === "all" ? undefined : filters.isActive,
+ sort_by: filters.sortBy,
+ sort_direction: filters.sortDirection,
+ });
+ }, [fetchApps, filters]);
+
+ if (isLoading) {
+ return (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ if (apps.length === 0) {
+ return (
+
+ No apps found matching your filters
+
+ );
+ }
+
+ return (
+
+ {apps.map((app) => (
+
+ ))}
+
+ );
+}
diff --git a/openmemory/ui/app/apps/page.tsx b/openmemory/ui/app/apps/page.tsx
new file mode 100644
index 00000000..9b255fe1
--- /dev/null
+++ b/openmemory/ui/app/apps/page.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { AppFilters } from "./components/AppFilters";
+import { AppGrid } from "./components/AppGrid";
+import "@/styles/animation.css";
+
+export default function AppsPage() {
+ return (
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/globals.css b/openmemory/ui/app/globals.css
new file mode 100644
index 00000000..91d1eb5a
--- /dev/null
+++ b/openmemory/ui/app/globals.css
@@ -0,0 +1,59 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 240 10% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 260 94% 59%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 240 3.7% 15.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 260 94% 59%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 240 10% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 260 94% 59%;
+ --primary-foreground: 355.7 100% 97.3%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 240 3.7% 15.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 260 94% 59%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/openmemory/ui/app/layout.tsx b/openmemory/ui/app/layout.tsx
new file mode 100644
index 00000000..0096136c
--- /dev/null
+++ b/openmemory/ui/app/layout.tsx
@@ -0,0 +1,38 @@
+import type React from "react";
+import "@/app/globals.css";
+import { ThemeProvider } from "@/components/theme-provider";
+import { Navbar } from "@/components/Navbar";
+import { Toaster } from "@/components/ui/toaster";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Providers } from "./providers";
+
+export const metadata = {
+ title: "OpenMemory - Developer Dashboard",
+ description: "Manage your OpenMemory integration and stored memories",
+ generator: "v0.dev",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/loading.tsx b/openmemory/ui/app/loading.tsx
new file mode 100644
index 00000000..4349ac3a
--- /dev/null
+++ b/openmemory/ui/app/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/openmemory/ui/app/memories/components/CreateMemoryDialog.tsx b/openmemory/ui/app/memories/components/CreateMemoryDialog.tsx
new file mode 100644
index 00000000..7eb4c821
--- /dev/null
+++ b/openmemory/ui/app/memories/components/CreateMemoryDialog.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Label } from "@/components/ui/label";
+import { useState, useRef } from "react";
+import { GoPlus } from "react-icons/go";
+import { Loader2 } from "lucide-react";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import { toast } from "sonner";
+import { Textarea } from "@/components/ui/textarea";
+
+export function CreateMemoryDialog() {
+ const { createMemory, isLoading, fetchMemories } = useMemoriesApi();
+ const [open, setOpen] = useState(false);
+ const textRef = useRef(null);
+
+ const handleCreateMemory = async (text: string) => {
+ try {
+ await createMemory(text);
+ toast.success("Memory created successfully");
+ // close the dialog
+ setOpen(false);
+ // refetch memories
+ await fetchMemories();
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to create memory");
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/openmemory/ui/app/memories/components/FilterComponent.tsx b/openmemory/ui/app/memories/components/FilterComponent.tsx
new file mode 100644
index 00000000..53d52b93
--- /dev/null
+++ b/openmemory/ui/app/memories/components/FilterComponent.tsx
@@ -0,0 +1,411 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Filter, X, ChevronDown, SortAsc, SortDesc } from "lucide-react";
+import { useDispatch, useSelector } from "react-redux";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuGroup,
+} from "@/components/ui/dropdown-menu";
+import { RootState } from "@/store/store";
+import { useAppsApi } from "@/hooks/useAppsApi";
+import { useFiltersApi } from "@/hooks/useFiltersApi";
+import {
+ setSelectedApps,
+ setSelectedCategories,
+ clearFilters,
+} from "@/store/filtersSlice";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+
+const columns = [
+ {
+ label: "Memory",
+ value: "memory",
+ },
+ {
+ label: "App Name",
+ value: "app_name",
+ },
+ {
+ label: "Created On",
+ value: "created_at",
+ },
+];
+
+export default function FilterComponent() {
+ const dispatch = useDispatch();
+ const { fetchApps } = useAppsApi();
+ const { fetchCategories, updateSort } = useFiltersApi();
+ const { fetchMemories } = useMemoriesApi();
+ const [isOpen, setIsOpen] = useState(false);
+ const [tempSelectedApps, setTempSelectedApps] = useState([]);
+ const [tempSelectedCategories, setTempSelectedCategories] = useState<
+ string[]
+ >([]);
+ const [showArchived, setShowArchived] = useState(false);
+
+ const apps = useSelector((state: RootState) => state.apps.apps);
+ const categories = useSelector(
+ (state: RootState) => state.filters.categories.items
+ );
+ const filters = useSelector((state: RootState) => state.filters.apps);
+
+ useEffect(() => {
+ fetchApps();
+ fetchCategories();
+ }, [fetchApps, fetchCategories]);
+
+ useEffect(() => {
+ // Initialize temporary selections with current active filters when dialog opens
+ if (isOpen) {
+ setTempSelectedApps(filters.selectedApps);
+ setTempSelectedCategories(filters.selectedCategories);
+ setShowArchived(filters.showArchived || false);
+ }
+ }, [isOpen, filters]);
+
+ useEffect(() => {
+ handleClearFilters();
+ }, []);
+
+ const toggleAppFilter = (app: string) => {
+ setTempSelectedApps((prev) =>
+ prev.includes(app) ? prev.filter((a) => a !== app) : [...prev, app]
+ );
+ };
+
+ const toggleCategoryFilter = (category: string) => {
+ setTempSelectedCategories((prev) =>
+ prev.includes(category)
+ ? prev.filter((c) => c !== category)
+ : [...prev, category]
+ );
+ };
+
+ const toggleAllApps = (checked: boolean) => {
+ setTempSelectedApps(checked ? apps.map((app) => app.id) : []);
+ };
+
+ const toggleAllCategories = (checked: boolean) => {
+ setTempSelectedCategories(checked ? categories.map((cat) => cat.name) : []);
+ };
+
+ const handleClearFilters = async () => {
+ setTempSelectedApps([]);
+ setTempSelectedCategories([]);
+ setShowArchived(false);
+ dispatch(clearFilters());
+ await fetchMemories();
+ };
+
+ const handleApplyFilters = async () => {
+ try {
+ // Get category IDs for selected category names
+ const selectedCategoryIds = categories
+ .filter((cat) => tempSelectedCategories.includes(cat.name))
+ .map((cat) => cat.id);
+
+ // Get app IDs for selected app names
+ const selectedAppIds = apps
+ .filter((app) => tempSelectedApps.includes(app.id))
+ .map((app) => app.id);
+
+ // Update the global state with temporary selections
+ dispatch(setSelectedApps(tempSelectedApps));
+ dispatch(setSelectedCategories(tempSelectedCategories));
+ dispatch({ type: "filters/setShowArchived", payload: showArchived });
+
+ await fetchMemories(undefined, 1, 10, {
+ apps: selectedAppIds,
+ categories: selectedCategoryIds,
+ sortColumn: filters.sortColumn,
+ sortDirection: filters.sortDirection,
+ showArchived: showArchived,
+ });
+ setIsOpen(false);
+ } catch (error) {
+ console.error("Failed to apply filters:", error);
+ }
+ };
+
+ const handleDialogChange = (open: boolean) => {
+ setIsOpen(open);
+ if (!open) {
+ // Reset temporary selections to active filters when dialog closes without applying
+ setTempSelectedApps(filters.selectedApps);
+ setTempSelectedCategories(filters.selectedCategories);
+ setShowArchived(filters.showArchived || false);
+ }
+ };
+
+ const setSorting = async (column: string) => {
+ const newDirection =
+ filters.sortColumn === column && filters.sortDirection === "asc"
+ ? "desc"
+ : "asc";
+ updateSort(column, newDirection);
+
+ // Get category IDs for selected category names
+ const selectedCategoryIds = categories
+ .filter((cat) => tempSelectedCategories.includes(cat.name))
+ .map((cat) => cat.id);
+
+ // Get app IDs for selected app names
+ const selectedAppIds = apps
+ .filter((app) => tempSelectedApps.includes(app.id))
+ .map((app) => app.id);
+
+ try {
+ await fetchMemories(undefined, 1, 10, {
+ apps: selectedAppIds,
+ categories: selectedCategoryIds,
+ sortColumn: column,
+ sortDirection: newDirection,
+ });
+ } catch (error) {
+ console.error("Failed to apply sorting:", error);
+ }
+ };
+
+ const hasActiveFilters =
+ filters.selectedApps.length > 0 ||
+ filters.selectedCategories.length > 0 ||
+ filters.showArchived;
+
+ const hasTempFilters =
+ tempSelectedApps.length > 0 ||
+ tempSelectedCategories.length > 0 ||
+ showArchived;
+
+ return (
+
+
+
+
+
+
+
+
+ Sort by
+
+
+ {columns.map((column) => (
+ setSorting(column.value)}
+ className="cursor-pointer flex justify-between items-center"
+ >
+ {column.label}
+ {filters.sortColumn === column.value &&
+ (filters.sortDirection === "asc" ? (
+
+ ) : (
+
+ ))}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/memories/components/MemoriesSection.tsx b/openmemory/ui/app/memories/components/MemoriesSection.tsx
new file mode 100644
index 00000000..08b0f6fe
--- /dev/null
+++ b/openmemory/ui/app/memories/components/MemoriesSection.tsx
@@ -0,0 +1,145 @@
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Category, Client } from "../../../components/types";
+import { MemoryTable } from "./MemoryTable";
+import { MemoryPagination } from "./MemoryPagination";
+import { CreateMemoryDialog } from "./CreateMemoryDialog";
+import { PageSizeSelector } from "./PageSizeSelector";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import { useRouter, useSearchParams } from "next/navigation";
+import { MemoryTableSkeleton } from "@/skeleton/MemoryTableSkeleton";
+
+export function MemoriesSection() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const { fetchMemories } = useMemoriesApi();
+ const [memories, setMemories] = useState([]);
+ const [totalItems, setTotalItems] = useState(0);
+ const [totalPages, setTotalPages] = useState(1);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const currentPage = Number(searchParams.get("page")) || 1;
+ const itemsPerPage = Number(searchParams.get("size")) || 10;
+ const [selectedCategory, setSelectedCategory] = useState(
+ "all"
+ );
+ const [selectedClient, setSelectedClient] = useState("all");
+
+ useEffect(() => {
+ const loadMemories = async () => {
+ setIsLoading(true);
+ try {
+ const searchQuery = searchParams.get("search") || "";
+ const result = await fetchMemories(
+ searchQuery,
+ currentPage,
+ itemsPerPage
+ );
+ setMemories(result.memories);
+ setTotalItems(result.total);
+ setTotalPages(result.pages);
+ } catch (error) {
+ console.error("Failed to fetch memories:", error);
+ }
+ setIsLoading(false);
+ };
+
+ loadMemories();
+ }, [currentPage, itemsPerPage, fetchMemories, searchParams]);
+
+ const setCurrentPage = (page: number) => {
+ const params = new URLSearchParams(searchParams.toString());
+ params.set("page", page.toString());
+ params.set("size", itemsPerPage.toString());
+ router.push(`?${params.toString()}`);
+ };
+
+ const handlePageSizeChange = (size: number) => {
+ const params = new URLSearchParams(searchParams.toString());
+ params.set("page", "1"); // Reset to page 1 when changing page size
+ params.set("size", size.toString());
+ router.push(`?${params.toString()}`);
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {memories.length > 0 ? (
+ <>
+
+
+
+
+ Showing {(currentPage - 1) * itemsPerPage + 1} to{" "}
+ {Math.min(currentPage * itemsPerPage, totalItems)} of{" "}
+ {totalItems} memories
+
+
+
+ >
+ ) : (
+
+
+
No memories found
+
+ {selectedCategory !== "all" || selectedClient !== "all"
+ ? "Try adjusting your filters"
+ : "Create your first memory to see it here"}
+
+ {selectedCategory !== "all" || selectedClient !== "all" ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/openmemory/ui/app/memories/components/MemoryFilters.tsx b/openmemory/ui/app/memories/components/MemoryFilters.tsx
new file mode 100644
index 00000000..8406162c
--- /dev/null
+++ b/openmemory/ui/app/memories/components/MemoryFilters.tsx
@@ -0,0 +1,154 @@
+"use client";
+import { Archive, Pause, Play, Search } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { FiTrash2 } from "react-icons/fi";
+import { useSelector, useDispatch } from "react-redux";
+import { RootState } from "@/store/store";
+import { clearSelection } from "@/store/memoriesSlice";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useRouter, useSearchParams } from "next/navigation";
+import { debounce } from "lodash";
+import { useEffect, useRef } from "react";
+import FilterComponent from "./FilterComponent";
+import { clearFilters } from "@/store/filtersSlice";
+
+export function MemoryFilters() {
+ const dispatch = useDispatch();
+ const selectedMemoryIds = useSelector(
+ (state: RootState) => state.memories.selectedMemoryIds
+ );
+ const { deleteMemories, updateMemoryState, fetchMemories } = useMemoriesApi();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const activeFilters = useSelector((state: RootState) => state.filters.apps);
+
+ const inputRef = useRef(null);
+
+ const handleDeleteSelected = async () => {
+ try {
+ await deleteMemories(selectedMemoryIds);
+ dispatch(clearSelection());
+ } catch (error) {
+ console.error("Failed to delete memories:", error);
+ }
+ };
+
+ const handleArchiveSelected = async () => {
+ try {
+ await updateMemoryState(selectedMemoryIds, "archived");
+ } catch (error) {
+ console.error("Failed to archive memories:", error);
+ }
+ };
+
+ const handlePauseSelected = async () => {
+ try {
+ await updateMemoryState(selectedMemoryIds, "paused");
+ } catch (error) {
+ console.error("Failed to pause memories:", error);
+ }
+ };
+
+ const handleResumeSelected = async () => {
+ try {
+ await updateMemoryState(selectedMemoryIds, "active");
+ } catch (error) {
+ console.error("Failed to resume memories:", error);
+ }
+ };
+
+ // add debounce
+ const handleSearch = debounce(async (query: string) => {
+ router.push(`/memories?search=${query}`);
+ }, 500);
+
+ useEffect(() => {
+ // if the url has a search param, set the input value to the search param
+ if (searchParams.get("search")) {
+ if (inputRef.current) {
+ inputRef.current.value = searchParams.get("search") || "";
+ inputRef.current.focus();
+ }
+ }
+ }, []);
+
+ const handleClearAllFilters = async () => {
+ dispatch(clearFilters());
+ await fetchMemories(); // Fetch memories without any filters
+ };
+
+ const hasActiveFilters =
+ activeFilters.selectedApps.length > 0 ||
+ activeFilters.selectedCategories.length > 0;
+
+ return (
+
+
+
+ handleSearch(e.target.value)}
+ />
+
+
+
+ {hasActiveFilters && (
+
+ )}
+ {selectedMemoryIds.length > 0 && (
+ <>
+
+
+
+
+
+
+
+ Archive Selected
+
+
+
+ Pause Selected
+
+
+
+ Resume Selected
+
+
+
+ Delete Selected
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/openmemory/ui/app/memories/components/MemoryPagination.tsx b/openmemory/ui/app/memories/components/MemoryPagination.tsx
new file mode 100644
index 00000000..f4a3e24b
--- /dev/null
+++ b/openmemory/ui/app/memories/components/MemoryPagination.tsx
@@ -0,0 +1,40 @@
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { Button } from "@/components/ui/button";
+
+interface MemoryPaginationProps {
+ currentPage: number;
+ totalPages: number;
+ setCurrentPage: (page: number) => void;
+}
+
+export function MemoryPagination({
+ currentPage,
+ totalPages,
+ setCurrentPage,
+}: MemoryPaginationProps) {
+ return (
+
+
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/memories/components/MemoryTable.tsx b/openmemory/ui/app/memories/components/MemoryTable.tsx
new file mode 100644
index 00000000..5ed95531
--- /dev/null
+++ b/openmemory/ui/app/memories/components/MemoryTable.tsx
@@ -0,0 +1,302 @@
+import {
+ Edit,
+ MoreHorizontal,
+ Trash2,
+ Pause,
+ Archive,
+ Play,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useToast } from "@/hooks/use-toast";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import { useDispatch, useSelector } from "react-redux";
+import { RootState } from "@/store/store";
+import {
+ selectMemory,
+ deselectMemory,
+ selectAllMemories,
+ clearSelection,
+} from "@/store/memoriesSlice";
+import SourceApp from "@/components/shared/source-app";
+import { HiMiniRectangleStack } from "react-icons/hi2";
+import { PiSwatches } from "react-icons/pi";
+import { GoPackage } from "react-icons/go";
+import { CiCalendar } from "react-icons/ci";
+import { useRouter } from "next/navigation";
+import Categories from "@/components/shared/categories";
+import { useUI } from "@/hooks/useUI";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { formatDate } from "@/lib/helpers";
+
+export function MemoryTable() {
+ const { toast } = useToast();
+ const router = useRouter();
+ const dispatch = useDispatch();
+ const selectedMemoryIds = useSelector(
+ (state: RootState) => state.memories.selectedMemoryIds
+ );
+ const memories = useSelector((state: RootState) => state.memories.memories);
+
+ const { deleteMemories, updateMemoryState, isLoading } = useMemoriesApi();
+
+ const handleDeleteMemory = (id: string) => {
+ deleteMemories([id]);
+ };
+
+ const handleSelectAll = (checked: boolean) => {
+ if (checked) {
+ dispatch(selectAllMemories());
+ } else {
+ dispatch(clearSelection());
+ }
+ };
+
+ const handleSelectMemory = (id: string, checked: boolean) => {
+ if (checked) {
+ dispatch(selectMemory(id));
+ } else {
+ dispatch(deselectMemory(id));
+ }
+ };
+ const { handleOpenUpdateMemoryDialog } = useUI();
+
+ const handleEditMemory = (memory_id: string, memory_content: string) => {
+ handleOpenUpdateMemoryDialog(memory_id, memory_content);
+ };
+
+ const handleUpdateMemoryState = async (id: string, newState: string) => {
+ try {
+ await updateMemoryState([id], newState);
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: "Failed to update memory state",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const isAllSelected =
+ memories.length > 0 && selectedMemoryIds.length === memories.length;
+ const isPartiallySelected =
+ selectedMemoryIds.length > 0 && selectedMemoryIds.length < memories.length;
+
+ const handleMemoryClick = (id: string) => {
+ router.push(`/memory/${id}`);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Memory
+
+
+
+
+
+
+
+
+ Source App
+
+
+
+
+
+ Created On
+
+
+
+
+
+
+
+
+
+
+ {memories.map((memory) => (
+
+
+
+ handleSelectMemory(memory.id, checked as boolean)
+ }
+ />
+
+
+ {memory.state === "paused" || memory.state === "archived" ? (
+
+
+
+ handleMemoryClick(memory.id)}
+ className={`font-medium ${
+ memory.state === "paused" ||
+ memory.state === "archived"
+ ? "text-zinc-400"
+ : "text-white"
+ } cursor-pointer`}
+ >
+ {memory.memory}
+
+
+
+
+ This memory is{" "}
+
+ {memory.state === "paused" ? "paused" : "archived"}
+ {" "}
+ and disabled.
+
+
+
+
+ ) : (
+ handleMemoryClick(memory.id)}
+ className={`font-medium text-white cursor-pointer`}
+ >
+ {memory.memory}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {formatDate(memory.created_at)}
+
+
+
+
+
+
+
+ {
+ const newState =
+ memory.state === "active" ? "paused" : "active";
+ handleUpdateMemoryState(memory.id, newState);
+ }}
+ >
+ {memory?.state === "active" ? (
+ <>
+
+ Pause
+ >
+ ) : (
+ <>
+
+ Resume
+ >
+ )}
+
+ {
+ const newState =
+ memory.state === "active" ? "archived" : "active";
+ handleUpdateMemoryState(memory.id, newState);
+ }}
+ >
+
+ {memory?.state !== "archived" ? (
+ <>Archive>
+ ) : (
+ <>Unarchive>
+ )}
+
+ handleEditMemory(memory.id, memory.memory)}
+ >
+
+ Edit
+
+
+ handleDeleteMemory(memory.id)}
+ >
+
+ Delete
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/memories/components/PageSizeSelector.tsx b/openmemory/ui/app/memories/components/PageSizeSelector.tsx
new file mode 100644
index 00000000..3987d05e
--- /dev/null
+++ b/openmemory/ui/app/memories/components/PageSizeSelector.tsx
@@ -0,0 +1,43 @@
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+interface PageSizeSelectorProps {
+ pageSize: number;
+ onPageSizeChange: (size: number) => void;
+}
+
+export function PageSizeSelector({
+ pageSize,
+ onPageSizeChange,
+}: PageSizeSelectorProps) {
+ const pageSizeOptions = [10, 20, 50, 100];
+
+ return (
+
+ Show
+
+ items
+
+ );
+}
+
+export default PageSizeSelector;
diff --git a/openmemory/ui/app/memories/page.tsx b/openmemory/ui/app/memories/page.tsx
new file mode 100644
index 00000000..3ebb9764
--- /dev/null
+++ b/openmemory/ui/app/memories/page.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { useEffect } from "react";
+import { MemoriesSection } from "@/app/memories/components/MemoriesSection";
+import { MemoryFilters } from "@/app/memories/components/MemoryFilters";
+import { useRouter, useSearchParams } from "next/navigation";
+import "@/styles/animation.css";
+import UpdateMemory from "@/components/shared/update-memory";
+import { useUI } from "@/hooks/useUI";
+
+export default function MemoriesPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const { updateMemoryDialog, handleCloseUpdateMemoryDialog } = useUI();
+ useEffect(() => {
+ // Set default pagination values if not present in URL
+ if (!searchParams.has("page") || !searchParams.has("size")) {
+ const params = new URLSearchParams(searchParams.toString());
+ if (!searchParams.has("page")) params.set("page", "1");
+ if (!searchParams.has("size")) params.set("size", "10");
+ router.push(`?${params.toString()}`);
+ }
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/openmemory/ui/app/memory/[id]/components/AccessLog.tsx b/openmemory/ui/app/memory/[id]/components/AccessLog.tsx
new file mode 100644
index 00000000..9d1b4d5f
--- /dev/null
+++ b/openmemory/ui/app/memory/[id]/components/AccessLog.tsx
@@ -0,0 +1,116 @@
+import Image from "next/image";
+import { useEffect, useState } from "react";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import { constants } from "@/components/shared/source-app";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store/store";
+import { ScrollArea } from "@/components/ui/scroll-area";
+
+interface AccessLogEntry {
+ id: string;
+ app_name: string;
+ accessed_at: string;
+}
+
+interface AccessLogProps {
+ memoryId: string;
+}
+
+export function AccessLog({ memoryId }: AccessLogProps) {
+ const { fetchAccessLogs } = useMemoriesApi();
+ const accessEntries = useSelector(
+ (state: RootState) => state.memories.accessLogs
+ );
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const loadAccessLogs = async () => {
+ try {
+ await fetchAccessLogs(memoryId);
+ } catch (error) {
+ console.error("Failed to fetch access logs:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadAccessLogs();
+ }, []);
+
+ if (isLoading) {
+ return (
+
+
Loading access logs...
+
+ );
+ }
+
+ return (
+
+
+
Access Log
+ {/*
*/}
+
+
+
+ {accessEntries.length === 0 && (
+
+
+ No access logs available
+
+
+ )}
+
+ {accessEntries.map((entry: AccessLogEntry, index: number) => {
+ const appConfig =
+ constants[entry.app_name as keyof typeof constants] ||
+ constants.default;
+
+ return (
+ -
+
+ {appConfig.iconImage ? (
+
+ ) : (
+
+ {appConfig.icon}
+
+ )}
+
+
+ {index < accessEntries.length - 1 && (
+
+ )}
+
+
+ {appConfig.name}
+
+ {new Date(entry.accessed_at + "Z").toLocaleDateString(
+ "en-US",
+ {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ }
+ )}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/memory/[id]/components/MemoryActions.tsx b/openmemory/ui/app/memory/[id]/components/MemoryActions.tsx
new file mode 100644
index 00000000..9f4958e8
--- /dev/null
+++ b/openmemory/ui/app/memory/[id]/components/MemoryActions.tsx
@@ -0,0 +1,114 @@
+import { Button } from "@/components/ui/button";
+import { Pencil, Archive, Trash, Pause, Play, ChevronDown } from "lucide-react";
+import { useUI } from "@/hooks/useUI";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+} from "@/components/ui/dropdown-menu";
+
+interface MemoryActionsProps {
+ memoryId: string;
+ memoryContent: string;
+ memoryState: string;
+}
+
+export function MemoryActions({
+ memoryId,
+ memoryContent,
+ memoryState,
+}: MemoryActionsProps) {
+ const { handleOpenUpdateMemoryDialog } = useUI();
+ const { updateMemoryState, isLoading } = useMemoriesApi();
+
+ const handleEdit = () => {
+ handleOpenUpdateMemoryDialog(memoryId, memoryContent);
+ };
+
+ const handleStateChange = (newState: string) => {
+ updateMemoryState([memoryId], newState);
+ };
+
+ const getStateLabel = () => {
+ switch (memoryState) {
+ case "archived":
+ return "Archived";
+ case "paused":
+ return "Paused";
+ default:
+ return "Active";
+ }
+ };
+
+ const getStateIcon = () => {
+ switch (memoryState) {
+ case "archived":
+ return ;
+ case "paused":
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Change State
+
+ handleStateChange("active")}
+ className="cursor-pointer flex items-center"
+ disabled={memoryState === "active"}
+ >
+
+ Active
+
+ handleStateChange("paused")}
+ className="cursor-pointer flex items-center"
+ disabled={memoryState === "paused"}
+ >
+
+ Pause
+
+ handleStateChange("archived")}
+ className="cursor-pointer flex items-center"
+ disabled={memoryState === "archived"}
+ >
+
+ Archive
+
+
+
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/memory/[id]/components/MemoryDetails.tsx b/openmemory/ui/app/memory/[id]/components/MemoryDetails.tsx
new file mode 100644
index 00000000..886aac6e
--- /dev/null
+++ b/openmemory/ui/app/memory/[id]/components/MemoryDetails.tsx
@@ -0,0 +1,151 @@
+"use client";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import { MemoryActions } from "./MemoryActions";
+import { ArrowLeft, Copy, Check } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useRouter } from "next/navigation";
+import { AccessLog } from "./AccessLog";
+import Image from "next/image";
+import Categories from "@/components/shared/categories";
+import { useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store/store";
+import { constants } from "@/components/shared/source-app";
+import { RelatedMemories } from "./RelatedMemories";
+
+interface MemoryDetailsProps {
+ memory_id: string;
+}
+
+export function MemoryDetails({ memory_id }: MemoryDetailsProps) {
+ const router = useRouter();
+ const { fetchMemoryById, hasUpdates } = useMemoriesApi();
+ const memory = useSelector(
+ (state: RootState) => state.memories.selectedMemory
+ );
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async () => {
+ if (memory?.id) {
+ await navigator.clipboard.writeText(memory.id);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ useEffect(() => {
+ fetchMemoryById(memory_id);
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+ Memory{" "}
+
+ #{memory?.id?.slice(0, 6)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Created by:
+
+
+
+
+
+ {
+ constants[
+ memory?.app_name as keyof typeof constants
+ ]?.name
+ }
+
+
+
+
+
+
+ {/*
+
+ {new Date(memory.created_at).toLocaleString()}
+
+
*/}
+
+
+
+
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/memory/[id]/components/RelatedMemories.tsx b/openmemory/ui/app/memory/[id]/components/RelatedMemories.tsx
new file mode 100644
index 00000000..0c681c04
--- /dev/null
+++ b/openmemory/ui/app/memory/[id]/components/RelatedMemories.tsx
@@ -0,0 +1,90 @@
+import { useEffect, useState } from "react";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store/store";
+import { Memory } from "@/components/types";
+import Categories from "@/components/shared/categories";
+import Link from "next/link";
+import { formatDate } from "@/lib/helpers";
+interface RelatedMemoriesProps {
+ memoryId: string;
+}
+
+export function RelatedMemories({ memoryId }: RelatedMemoriesProps) {
+ const { fetchRelatedMemories } = useMemoriesApi();
+ const relatedMemories = useSelector(
+ (state: RootState) => state.memories.relatedMemories
+ );
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const loadRelatedMemories = async () => {
+ try {
+ await fetchRelatedMemories(memoryId);
+ } catch (error) {
+ console.error("Failed to fetch related memories:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadRelatedMemories();
+ }, []);
+
+ if (isLoading) {
+ return (
+
+
Loading related memories...
+
+ );
+ }
+
+ if (!relatedMemories.length) {
+ return (
+
+
No related memories found
+
+ );
+ }
+
+ return (
+
+
+
Related Memories
+
+
+ {relatedMemories.map((memory: Memory) => (
+
+
+
{memory.memory}
+
+
+
+ {memory.state !== "active" && (
+
+ {memory.state === "paused" ? "Paused" : "Archived"}
+
+ )}
+
+
+
+ {formatDate(memory.created_at)}
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/openmemory/ui/app/memory/[id]/page.tsx b/openmemory/ui/app/memory/[id]/page.tsx
new file mode 100644
index 00000000..310999ec
--- /dev/null
+++ b/openmemory/ui/app/memory/[id]/page.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import "@/styles/animation.css";
+import { useEffect } from "react";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import { use } from "react";
+import { MemorySkeleton } from "@/skeleton/MemorySkeleton";
+import { MemoryDetails } from "./components/MemoryDetails";
+import UpdateMemory from "@/components/shared/update-memory";
+import { useUI } from "@/hooks/useUI";
+import { RootState } from "@/store/store";
+import { useSelector } from "react-redux";
+import NotFound from "@/app/not-found";
+
+function MemoryContent({ id }: { id: string }) {
+ const { fetchMemoryById, isLoading, error } = useMemoriesApi();
+ const memory = useSelector(
+ (state: RootState) => state.memories.selectedMemory
+ );
+
+ useEffect(() => {
+ const loadMemory = async () => {
+ try {
+ await fetchMemoryById(id);
+ } catch (err) {
+ console.error("Failed to load memory:", err);
+ }
+ };
+ loadMemory();
+ }, []);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
+ if (!memory) {
+ return ;
+ }
+
+ return ;
+}
+
+export default function MemoryPage({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const resolvedParams = use(params);
+ const { updateMemoryDialog, handleCloseUpdateMemoryDialog } = useUI();
+ return (
+
+ );
+}
diff --git a/openmemory/ui/app/not-found.tsx b/openmemory/ui/app/not-found.tsx
new file mode 100644
index 00000000..151d5f0c
--- /dev/null
+++ b/openmemory/ui/app/not-found.tsx
@@ -0,0 +1,53 @@
+import "@/styles/notfound.scss";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+
+interface NotFoundProps {
+ statusCode?: number;
+ message?: string;
+ title?: string;
+}
+
+const getStatusCode = (message: string) => {
+ const possibleStatusCodes = ["404", "403", "500", "422"];
+ const potentialStatusCode = possibleStatusCodes.find((code) =>
+ message.includes(code)
+ );
+ return potentialStatusCode ? parseInt(potentialStatusCode) : undefined;
+};
+
+export default function NotFound({
+ statusCode,
+ message = "Page Not Found",
+ title,
+}: NotFoundProps) {
+ const potentialStatusCode = getStatusCode(message);
+
+ return (
+
+
+
+
+ {statusCode
+ ? `${statusCode}:`
+ : potentialStatusCode
+ ? `${potentialStatusCode}:`
+ : "404"}
+ {title || message || "Page Not Found"}
+
+
+
+
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/page.tsx b/openmemory/ui/app/page.tsx
new file mode 100644
index 00000000..f905ee14
--- /dev/null
+++ b/openmemory/ui/app/page.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { Install } from "@/components/dashboard/Install";
+import Stats from "@/components/dashboard/Stats";
+import { MemoryFilters } from "@/app/memories/components/MemoryFilters";
+import { MemoriesSection } from "@/app/memories/components/MemoriesSection";
+import "@/styles/animation.css";
+
+export default function DashboardPage() {
+ return (
+
+
+
+
+ {/* Memory Category Breakdown */}
+
+
+
+
+ {/* Memories Stats */}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/openmemory/ui/app/providers.tsx b/openmemory/ui/app/providers.tsx
new file mode 100644
index 00000000..068c4b9d
--- /dev/null
+++ b/openmemory/ui/app/providers.tsx
@@ -0,0 +1,8 @@
+"use client";
+
+import { Provider } from "react-redux";
+import { store } from "../store/store";
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ return {children};
+}
diff --git a/openmemory/ui/components.json b/openmemory/ui/components.json
new file mode 100644
index 00000000..d9ef0ae5
--- /dev/null
+++ b/openmemory/ui/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/openmemory/ui/components/Navbar.tsx b/openmemory/ui/components/Navbar.tsx
new file mode 100644
index 00000000..34524ff0
--- /dev/null
+++ b/openmemory/ui/components/Navbar.tsx
@@ -0,0 +1,146 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { HiHome, HiMiniRectangleStack } from "react-icons/hi2";
+import { RiApps2AddFill } from "react-icons/ri";
+import { FiRefreshCcw } from "react-icons/fi";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { CreateMemoryDialog } from "@/app/memories/components/CreateMemoryDialog";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import Image from "next/image";
+import { useStats } from "@/hooks/useStats";
+import { useAppsApi } from "@/hooks/useAppsApi";
+
+export function Navbar() {
+ const pathname = usePathname();
+
+ const memoriesApi = useMemoriesApi();
+ const appsApi = useAppsApi();
+ const statsApi = useStats();
+
+ // Define route matchers with typed parameter extraction
+ const routeBasedFetchMapping: {
+ match: RegExp;
+ getFetchers: (params: Record) => (() => Promise)[];
+ }[] = [
+ {
+ match: /^\/memory\/([^/]+)$/,
+ getFetchers: ({ memory_id }) => [
+ () => memoriesApi.fetchMemoryById(memory_id),
+ () => memoriesApi.fetchAccessLogs(memory_id),
+ () => memoriesApi.fetchRelatedMemories(memory_id),
+ ],
+ },
+ {
+ match: /^\/apps\/([^/]+)$/,
+ getFetchers: ({ app_id }) => [
+ () => appsApi.fetchAppMemories(app_id),
+ () => appsApi.fetchAppAccessedMemories(app_id),
+ () => appsApi.fetchAppDetails(app_id),
+ ],
+ },
+ {
+ match: /^\/memories$/,
+ getFetchers: () => [memoriesApi.fetchMemories],
+ },
+ {
+ match: /^\/apps$/,
+ getFetchers: () => [appsApi.fetchApps],
+ },
+ {
+ match: /^\/$/,
+ getFetchers: () => [statsApi.fetchStats, memoriesApi.fetchMemories],
+ },
+ ];
+
+ const getFetchersForPath = (path: string) => {
+ for (const route of routeBasedFetchMapping) {
+ const match = path.match(route.match);
+ if (match) {
+ if (route.match.source.includes("memory")) {
+ return route.getFetchers({ memory_id: match[1] });
+ }
+ if (route.match.source.includes("app")) {
+ return route.getFetchers({ app_id: match[1] });
+ }
+ return route.getFetchers({});
+ }
+ }
+ return [];
+ };
+
+ const handleRefresh = async () => {
+ const fetchers = getFetchersForPath(pathname);
+ await Promise.allSettled(fetchers.map((fn) => fn()));
+ };
+
+ const isActive = (href: string) => {
+ if (href === "/") return pathname === href;
+ return pathname.startsWith(href.substring(0, 5));
+ };
+
+ const activeClass = "bg-zinc-800 text-white border-zinc-600";
+ const inactiveClass = "text-zinc-300";
+
+ return (
+
+ );
+}
diff --git a/openmemory/ui/components/dashboard/Install.tsx b/openmemory/ui/components/dashboard/Install.tsx
new file mode 100644
index 00000000..27aa7741
--- /dev/null
+++ b/openmemory/ui/components/dashboard/Install.tsx
@@ -0,0 +1,196 @@
+"use client";
+
+import React, { useState } from "react";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Copy, Check } from "lucide-react";
+import Image from "next/image";
+
+const clientTabs = [
+ { key: "claude", label: "Claude", icon: "/images/claude.webp" },
+ { key: "cursor", label: "Cursor", icon: "/images/cursor.png" },
+ { key: "cline", label: "Cline", icon: "/images/cline.png" },
+ { key: "roocline", label: "Roo Cline", icon: "/images/roocline.png" },
+ { key: "windsurf", label: "Windsurf", icon: "/images/windsurf.png" },
+ { key: "witsy", label: "Witsy", icon: "/images/witsy.png" },
+ { key: "enconvo", label: "Enconvo", icon: "/images/enconvo.png" },
+];
+
+const colorGradientMap: { [key: string]: string } = {
+ claude:
+ "data-[state=active]:bg-[linear-gradient(to_top,_rgba(239,108,60,0.3),_rgba(239,108,60,0))] data-[state=active]:border-[#EF6C3C]",
+ cline:
+ "data-[state=active]:bg-[linear-gradient(to_top,_rgba(112,128,144,0.3),_rgba(112,128,144,0))] data-[state=active]:border-[#708090]",
+ cursor:
+ "data-[state=active]:bg-[linear-gradient(to_top,_rgba(255,255,255,0.08),_rgba(255,255,255,0))] data-[state=active]:border-[#708090]",
+ roocline:
+ "data-[state=active]:bg-[linear-gradient(to_top,_rgba(45,32,92,0.8),_rgba(45,32,92,0))] data-[state=active]:border-[#7E3FF2]",
+ windsurf:
+ "data-[state=active]:bg-[linear-gradient(to_top,_rgba(0,176,137,0.3),_rgba(0,176,137,0))] data-[state=active]:border-[#00B089]",
+ witsy:
+ "data-[state=active]:bg-[linear-gradient(to_top,_rgba(33,135,255,0.3),_rgba(33,135,255,0))] data-[state=active]:border-[#2187FF]",
+ enconvo:
+ "data-[state=active]:bg-[linear-gradient(to_top,_rgba(126,63,242,0.3),_rgba(126,63,242,0))] data-[state=active]:border-[#7E3FF2]",
+};
+
+const getColorGradient = (color: string) => {
+ if (colorGradientMap[color]) {
+ return colorGradientMap[color];
+ }
+ return "data-[state=active]:bg-[linear-gradient(to_top,_rgba(126,63,242,0.3),_rgba(126,63,242,0))] data-[state=active]:border-[#7E3FF2]";
+};
+
+const allTabs = [{ key: "mcp", label: "MCP Link", icon: "🔗" }, ...clientTabs];
+
+export const Install = () => {
+ const [copiedTab, setCopiedTab] = useState(null);
+ const user = process.env.NEXT_PUBLIC_USER_ID || "user";
+
+ const URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
+
+ const handleCopy = async (tab: string, isMcp: boolean = false) => {
+ const text = isMcp
+ ? `${URL}/mcp/openmemory/sse/${user}`
+ : `npx install-mcp i ${URL}/mcp/${tab}/sse/${user} --client ${tab}`;
+
+ try {
+ // Try using the Clipboard API first
+ if (navigator?.clipboard?.writeText) {
+ await navigator.clipboard.writeText(text);
+ } else {
+ // Fallback: Create a temporary textarea element
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.style.position = "fixed";
+ textarea.style.opacity = "0";
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ }
+
+ // Update UI to show success
+ setCopiedTab(tab);
+ setTimeout(() => setCopiedTab(null), 1500); // Reset after 1.5s
+ } catch (error) {
+ console.error("Failed to copy text:", error);
+ // You might want to add a toast notification here to show the error
+ }
+ };
+
+ return (
+
+
Install OpenMemory
+
+
+
+
+
+ {allTabs.map(({ key, label, icon }) => (
+
+ {icon.startsWith("/") ? (
+
+ ) : (
+
+ {icon}
+
+ )}
+ {label}
+
+ ))}
+
+
+ {/* MCP Tab Content */}
+
+
+
+ MCP Link
+
+
+
+
+
+
+ {URL}/mcp/openmemory/sse/{user}
+
+
+
+
+
+
+
+
+
+
+ {/* Client Tabs Content */}
+ {clientTabs.map(({ key }) => (
+
+
+
+
+ {key.charAt(0).toUpperCase() + key.slice(1)} Installation
+ Command
+
+
+
+
+
+
+
+ {`npx install-mcp i ${URL}/mcp/${key}/sse/${user} --client ${key}`}
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+export default Install;
diff --git a/openmemory/ui/components/dashboard/Stats.tsx b/openmemory/ui/components/dashboard/Stats.tsx
new file mode 100644
index 00000000..b73ef412
--- /dev/null
+++ b/openmemory/ui/components/dashboard/Stats.tsx
@@ -0,0 +1,69 @@
+import React, { useEffect } from "react";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store/store";
+import { useStats } from "@/hooks/useStats";
+import Image from "next/image";
+import { constants } from "@/components/shared/source-app";
+const Stats = () => {
+ const totalMemories = useSelector(
+ (state: RootState) => state.profile.totalMemories
+ );
+ const totalApps = useSelector((state: RootState) => state.profile.totalApps);
+ const apps = useSelector((state: RootState) => state.profile.apps).slice(
+ 0,
+ 4
+ );
+ const { fetchStats } = useStats();
+
+ useEffect(() => {
+ fetchStats();
+ }, []);
+
+ return (
+
+
+
+
+
Total Memories
+
+ {totalMemories} Memories
+
+
+
+
Total Apps Connected
+
+
+ {apps.map((app) => (
+
+ ))}
+
+
{totalApps} Apps
+
+
+
+
+ );
+};
+
+export default Stats;
diff --git a/openmemory/ui/components/shared/categories.tsx b/openmemory/ui/components/shared/categories.tsx
new file mode 100644
index 00000000..3b4d06ce
--- /dev/null
+++ b/openmemory/ui/components/shared/categories.tsx
@@ -0,0 +1,230 @@
+import React, { useState } from "react";
+import {
+ Book,
+ HeartPulse,
+ BriefcaseBusiness,
+ CircleHelp,
+ Palette,
+ Code,
+ Settings,
+ Users,
+ Heart,
+ Brain,
+ MapPin,
+ Globe,
+ PersonStandingIcon,
+} from "lucide-react";
+import {
+ FaLaptopCode,
+ FaPaintBrush,
+ FaBusinessTime,
+ FaRegHeart,
+ FaRegSmile,
+ FaUserTie,
+ FaMoneyBillWave,
+ FaBriefcase,
+ FaPlaneDeparture,
+} from "react-icons/fa";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Badge } from "../ui/badge";
+
+type Category = string;
+
+const defaultIcon = ;
+
+const iconMap: Record = {
+ // Core themes
+ health: ,
+ wellness: ,
+ fitness: ,
+ education: ,
+ learning: ,
+ school: ,
+ coding: ,
+ programming: ,
+ development: ,
+ tech: ,
+ design: ,
+ art: ,
+ creativity: ,
+ psychology: ,
+ mental: ,
+ social: ,
+ peronsal: ,
+ life: ,
+
+ // Work / Career
+ business: ,
+ work: ,
+ career: ,
+ jobs: ,
+ finance: ,
+ money: ,
+
+ // Preferences
+ preference: ,
+ interest: ,
+
+ // Travel & Location
+ travel: ,
+ journey: ,
+ location: ,
+ trip: ,
+ places: ,
+};
+
+const getClosestIcon = (label: string): any => {
+ const normalized = label.toLowerCase().split(/[\s\-_.]+/);
+
+ let bestMatch: string | null = null;
+ let bestScore = 0;
+
+ Object.keys(iconMap).forEach((key) => {
+ const keyTokens = key.split(/[\s\-_.]+/);
+ const matchScore = normalized.filter((word) =>
+ keyTokens.some((token) => word.includes(token) || token.includes(word))
+ ).length;
+
+ if (matchScore > bestScore) {
+ bestScore = matchScore;
+ bestMatch = key;
+ }
+ });
+
+ return bestMatch ? iconMap[bestMatch] : defaultIcon;
+};
+
+const getColor = (label: string): string => {
+ const l = label.toLowerCase();
+ if (l.includes("health") || l.includes("fitness"))
+ return "text-emerald-400 bg-emerald-500/10 border-emerald-500/20";
+ if (l.includes("education") || l.includes("school"))
+ return "text-indigo-400 bg-indigo-500/10 border-indigo-500/20";
+ if (
+ l.includes("business") ||
+ l.includes("career") ||
+ l.includes("work") ||
+ l.includes("finance")
+ )
+ return "text-amber-400 bg-amber-500/10 border-amber-500/20";
+ if (l.includes("design") || l.includes("art") || l.includes("creative"))
+ return "text-pink-400 bg-pink-500/10 border-pink-500/20";
+ if (l.includes("tech") || l.includes("code") || l.includes("programming"))
+ return "text-purple-400 bg-purple-500/10 border-purple-500/20";
+ if (l.includes("interest") || l.includes("preference"))
+ return "text-rose-400 bg-rose-500/10 border-rose-500/20";
+ if (
+ l.includes("travel") ||
+ l.includes("trip") ||
+ l.includes("location") ||
+ l.includes("place")
+ )
+ return "text-sky-400 bg-sky-500/10 border-sky-500/20";
+ if (l.includes("personal") || l.includes("life"))
+ return "text-yellow-400 bg-yellow-500/10 border-yellow-500/20";
+ return "text-blue-400 bg-blue-500/10 border-blue-500/20";
+};
+
+const Categories = ({
+ categories,
+ isPaused = false,
+ concat = false,
+}: {
+ categories: Category[];
+ isPaused?: boolean;
+ concat?: boolean;
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ if (!categories || categories.length === 0) return null;
+
+ const baseBadgeStyle =
+ "backdrop-blur-sm transition-colors hover:bg-opacity-20";
+ const pausedStyle =
+ "text-zinc-500 bg-zinc-800/40 border-zinc-700/40 hover:bg-zinc-800/60";
+
+ if (concat) {
+ const remainingCount = categories.length - 1;
+
+ return (
+
+ {/* First category */}
+
+ {categories[0]}
+
+
+ {/* Popover for remaining categories */}
+ {remainingCount > 0 && (
+
+ setIsOpen(true)}
+ onMouseLeave={() => setIsOpen(false)}
+ >
+
+ +{remainingCount}
+
+
+ setIsOpen(true)}
+ onMouseLeave={() => setIsOpen(false)}
+ >
+
+ {categories.slice(1).map((cat, i) => (
+
+ {cat}
+
+ ))}
+
+
+
+ )}
+
+ );
+ }
+
+ // Default view
+ return (
+
+ {categories?.map((cat, i) => (
+
+ {cat}
+
+ ))}
+
+ );
+};
+
+export default Categories;
diff --git a/openmemory/ui/components/shared/source-app.tsx b/openmemory/ui/components/shared/source-app.tsx
new file mode 100644
index 00000000..374fd1b8
--- /dev/null
+++ b/openmemory/ui/components/shared/source-app.tsx
@@ -0,0 +1,80 @@
+import React from "react";
+import { BiEdit } from "react-icons/bi";
+import Image from "next/image";
+
+export const Icon = ({ source }: { source: string }) => {
+ return (
+
+
+
+ );
+};
+
+export const constants = {
+ claude: {
+ name: "Claude",
+ icon: ,
+ iconImage: "/images/claude.webp",
+ },
+ openmemory: {
+ name: "OpenMemory",
+ icon: ,
+ iconImage: "/images/open-memory.svg",
+ },
+ cursor: {
+ name: "Cursor",
+ icon: ,
+ iconImage: "/images/cursor.png",
+ },
+ cline: {
+ name: "Cline",
+ icon: ,
+ iconImage: "/images/cline.png",
+ },
+ roocline: {
+ name: "Roo Cline",
+ icon: ,
+ iconImage: "/images/roocline.png",
+ },
+ windsurf: {
+ name: "Windsurf",
+ icon: ,
+ iconImage: "/images/windsurf.png",
+ },
+ witsy: {
+ name: "Witsy",
+ icon: ,
+ iconImage: "/images/witsy.png",
+ },
+ enconvo: {
+ name: "Enconvo",
+ icon: ,
+ iconImage: "/images/enconvo.png",
+ },
+ default: {
+ name: "Default",
+ icon: ,
+ iconImage: "/images/default.png",
+ },
+};
+
+const SourceApp = ({ source }: { source: string }) => {
+ if (!constants[source as keyof typeof constants]) {
+ return (
+
+
+ {source}
+
+ );
+ }
+ return (
+
+ {constants[source as keyof typeof constants].icon}
+
+ {constants[source as keyof typeof constants].name}
+
+
+ );
+};
+
+export default SourceApp;
diff --git a/openmemory/ui/components/shared/update-memory.tsx b/openmemory/ui/components/shared/update-memory.tsx
new file mode 100644
index 00000000..aa09e59a
--- /dev/null
+++ b/openmemory/ui/components/shared/update-memory.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Label } from "@/components/ui/label";
+import { useRef } from "react";
+import { Loader2 } from "lucide-react";
+import { useMemoriesApi } from "@/hooks/useMemoriesApi";
+import { toast } from "sonner";
+import { Textarea } from "@/components/ui/textarea";
+import { usePathname } from "next/navigation";
+
+interface UpdateMemoryProps {
+ memoryId: string;
+ memoryContent: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+const UpdateMemory = ({
+ memoryId,
+ memoryContent,
+ open,
+ onOpenChange,
+}: UpdateMemoryProps) => {
+ const { updateMemory, isLoading, fetchMemories, fetchMemoryById } =
+ useMemoriesApi();
+ const textRef = useRef(null);
+ const pathname = usePathname();
+
+ const handleUpdateMemory = async (text: string) => {
+ try {
+ await updateMemory(memoryId, text);
+ toast.success("Memory updated successfully");
+ onOpenChange(false);
+ if (pathname.includes("memories")) {
+ await fetchMemories();
+ } else {
+ await fetchMemoryById(memoryId);
+ }
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to update memory");
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default UpdateMemory;
diff --git a/openmemory/ui/components/theme-provider.tsx b/openmemory/ui/components/theme-provider.tsx
new file mode 100644
index 00000000..1a44ac3a
--- /dev/null
+++ b/openmemory/ui/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import * as React from "react";
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from "next-themes";
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children};
+}
diff --git a/openmemory/ui/components/types.ts b/openmemory/ui/components/types.ts
new file mode 100644
index 00000000..2cebdb34
--- /dev/null
+++ b/openmemory/ui/components/types.ts
@@ -0,0 +1,13 @@
+export type Category = "personal" | "work" | "health" | "finance" | "travel" | "education" | "preferences" | "relationships"
+export type Client = "chrome" | "chatgpt" | "cursor" | "windsurf" | "terminal" | "api"
+
+export interface Memory {
+ id: string
+ memory: string
+ metadata: any
+ client: Client
+ categories: Category[]
+ created_at: number
+ app_name: string
+ state: "active" | "paused" | "archived" | "deleted"
+}
\ No newline at end of file
diff --git a/openmemory/ui/components/ui/accordion.tsx b/openmemory/ui/components/ui/accordion.tsx
new file mode 100644
index 00000000..24c788c2
--- /dev/null
+++ b/openmemory/ui/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/openmemory/ui/components/ui/alert-dialog.tsx b/openmemory/ui/components/ui/alert-dialog.tsx
new file mode 100644
index 00000000..25e7b474
--- /dev/null
+++ b/openmemory/ui/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/openmemory/ui/components/ui/alert.tsx b/openmemory/ui/components/ui/alert.tsx
new file mode 100644
index 00000000..41fa7e05
--- /dev/null
+++ b/openmemory/ui/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/openmemory/ui/components/ui/aspect-ratio.tsx b/openmemory/ui/components/ui/aspect-ratio.tsx
new file mode 100644
index 00000000..d6a5226f
--- /dev/null
+++ b/openmemory/ui/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/openmemory/ui/components/ui/avatar.tsx b/openmemory/ui/components/ui/avatar.tsx
new file mode 100644
index 00000000..51e507ba
--- /dev/null
+++ b/openmemory/ui/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/openmemory/ui/components/ui/badge.tsx b/openmemory/ui/components/ui/badge.tsx
new file mode 100644
index 00000000..f000e3ef
--- /dev/null
+++ b/openmemory/ui/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/openmemory/ui/components/ui/breadcrumb.tsx b/openmemory/ui/components/ui/breadcrumb.tsx
new file mode 100644
index 00000000..60e6c96f
--- /dev/null
+++ b/openmemory/ui/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/openmemory/ui/components/ui/button.tsx b/openmemory/ui/components/ui/button.tsx
new file mode 100644
index 00000000..36496a28
--- /dev/null
+++ b/openmemory/ui/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/openmemory/ui/components/ui/calendar.tsx b/openmemory/ui/components/ui/calendar.tsx
new file mode 100644
index 00000000..61d2b451
--- /dev/null
+++ b/openmemory/ui/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/openmemory/ui/components/ui/card.tsx b/openmemory/ui/components/ui/card.tsx
new file mode 100644
index 00000000..afa13ecf
--- /dev/null
+++ b/openmemory/ui/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/openmemory/ui/components/ui/carousel.tsx b/openmemory/ui/components/ui/carousel.tsx
new file mode 100644
index 00000000..ec505d00
--- /dev/null
+++ b/openmemory/ui/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/openmemory/ui/components/ui/chart.tsx b/openmemory/ui/components/ui/chart.tsx
new file mode 100644
index 00000000..8620baa3
--- /dev/null
+++ b/openmemory/ui/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+