Merge remote-tracking branch 'origin/main' into fix/docs-broken-links-ci

This commit is contained in:
James Yang 2026-09-02 19:44:18 -04:00
commit 9cf008586e
320 changed files with 17852 additions and 6764 deletions

257
.github/workflows/ci-python.yml vendored Normal file
View file

@ -0,0 +1,257 @@
name: CI - Python SDKs
on:
pull_request:
paths:
- "packages/agent-framework-python/**"
- "packages/cartesia-sdk-python/**"
- "packages/openai-sdk-python/**"
- "packages/pipecat-sdk-python/**"
- ".github/workflows/ci-python.yml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
jobs:
agent-framework-python:
name: agent-framework-python (${{ matrix.dependency-lane }}, Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.10"
dependency-lane: minimum-supermemory
supermemory-version: "3.16.0"
- python-version: "3.13"
dependency-lane: current-supermemory
supermemory-version: "3.59.0"
defaults:
run:
working-directory: packages/agent-framework-python
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: packages/agent-framework-python/pyproject.toml
- name: Install build and test tools
run: python -m pip install build pytest pytest-asyncio
- name: Build wheel
run: python -m build --wheel --outdir "$RUNNER_TEMP/wheels"
- name: Install wheel and tested Supermemory SDK
run: >-
python -m pip install "$RUNNER_TEMP"/wheels/*.whl
"supermemory==${{ matrix.supermemory-version }}"
- name: Check dependency compatibility
run: python -m pip check
- name: Verify installed wheel and SDK version
run: >-
python -c "from importlib.metadata import version; from pathlib import Path;
import supermemory_agent_framework;
assert version('supermemory') == '${{ matrix.supermemory-version }}';
assert 'site-packages' in Path(supermemory_agent_framework.__file__).parts"
- name: Run tests
run: python -m pytest
openai-sdk-python:
name: openai-sdk-python (${{ matrix.dependency-lane }}, Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.9"
dependency-lane: minimum-supermemory
supermemory-version: "3.50.0"
expected-supermemory-version: "3.50.0"
- python-version: "3.12"
dependency-lane: locked
supermemory-version: ""
expected-supermemory-version: "3.59.0"
defaults:
run:
working-directory: packages/openai-sdk-python
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Setup uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"
enable-cache: true
working-directory: packages/openai-sdk-python
cache-dependency-glob: uv.lock
- name: Install locked dependencies
run: uv sync --locked --python "${{ matrix.python-version }}"
- name: Build wheel
run: uv build --wheel --out-dir "$RUNNER_TEMP/wheels"
- name: Install built wheel
run: >-
uv pip install --python .venv/bin/python --reinstall --no-deps
"$RUNNER_TEMP"/wheels/*.whl
- name: Install minimum Supermemory SDK
if: matrix.supermemory-version != ''
run: >-
uv pip install --python .venv/bin/python
"supermemory==${{ matrix.supermemory-version }}"
- name: Check dependency compatibility
run: uv pip check --python .venv/bin/python
- name: Verify installed wheel and SDK version
run: >-
.venv/bin/python -c "from importlib.metadata import version;
from pathlib import Path; import supermemory_openai;
assert version('supermemory') == '${{ matrix.expected-supermemory-version }}';
assert 'site-packages' in Path(supermemory_openai.__file__).parts"
- name: Run tests without changing the verified environment
run: .venv/bin/python -m pytest
cartesia-sdk-python:
name: cartesia-sdk-python (${{ matrix.dependency-lane }}, Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.10"
dependency-lane: minimum-dependencies
supermemory-version: "3.16.0"
cartesia-line-version: "0.2.0"
- python-version: "3.12"
dependency-lane: current-dependencies
supermemory-version: "3.59.0"
cartesia-line-version: "0.2.17"
defaults:
run:
working-directory: packages/cartesia-sdk-python
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: packages/cartesia-sdk-python/pyproject.toml
- name: Install build and test tools
run: python -m pip install build pytest
- name: Build wheel
run: python -m build --wheel --outdir "$RUNNER_TEMP/wheels"
- name: Install wheel and tested runtime dependencies
run: >-
python -m pip install "$RUNNER_TEMP"/wheels/*.whl
"supermemory==${{ matrix.supermemory-version }}"
"cartesia-line==${{ matrix.cartesia-line-version }}"
- name: Check dependency compatibility
run: python -m pip check
- name: Verify real Cartesia Line integration and run tests
run: >-
python -c "from importlib.metadata import version;
from pathlib import Path; import line, pytest, supermemory_cartesia;
assert version('supermemory') == '${{ matrix.supermemory-version }}';
assert version('cartesia-line') == '${{ matrix.cartesia-line-version }}';
assert 'site-packages' in Path(supermemory_cartesia.__file__).parts;
result = pytest.main(['-W', 'ignore::pytest.PytestAssertRewriteWarning', 'tests']);
raise SystemExit(result)"
pipecat-sdk-python:
name: pipecat-sdk-python (${{ matrix.dependency-lane }}, Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.10"
dependency-lane: minimum-dependencies
supermemory-version: "3.16.0"
pipecat-version: "0.0.98"
- python-version: "3.12"
dependency-lane: current-dependencies
supermemory-version: "3.59.0"
pipecat-version: "1.7.0"
defaults:
run:
working-directory: packages/pipecat-sdk-python
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: packages/pipecat-sdk-python/pyproject.toml
- name: Install build and test tools
run: python -m pip install build pytest
- name: Build wheel
run: python -m build --wheel --outdir "$RUNNER_TEMP/wheels"
- name: Install wheel and tested runtime dependencies
run: >-
python -m pip install "$RUNNER_TEMP"/wheels/*.whl
"supermemory==${{ matrix.supermemory-version }}"
"pipecat-ai==${{ matrix.pipecat-version }}"
- name: Check dependency compatibility
run: python -m pip check
- name: Verify real Pipecat integration and run tests
run: >-
python -c "from importlib.metadata import version;
from pathlib import Path; import pipecat, pytest, supermemory_pipecat;
assert version('supermemory') == '${{ matrix.supermemory-version }}';
assert version('pipecat-ai') == '${{ matrix.pipecat-version }}';
assert 'site-packages' in Path(supermemory_pipecat.__file__).parts;
result = pytest.main(['-W', 'ignore::pytest.PytestAssertRewriteWarning', 'tests']);
raise SystemExit(result)"

View file

@ -26,8 +26,83 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run TypeScript type checking
run: bunx turbo run check-types --filter='@supermemory/ai-sdk' --filter='@supermemory/memory-graph'
- name: Detect SDK and playground changes
id: sdk-changes
run: |
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/tools; then
echo "tools=false" >> "$GITHUB_OUTPUT"
else
echo "tools=true" >> "$GITHUB_OUTPUT"
fi
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/ai-sdk; then
echo "ai_sdk=false" >> "$GITHUB_OUTPUT"
else
echo "ai_sdk=true" >> "$GITHUB_OUTPUT"
fi
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- apps/sdk-playground; then
echo "sdk_playground=false" >> "$GITHUB_OUTPUT"
else
echo "sdk_playground=true" >> "$GITHUB_OUTPUT"
fi
- name: Setup Python for SDK Playground
if: steps.sdk-changes.outputs.sdk_playground == 'true'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Setup uv for SDK Playground
if: steps.sdk-changes.outputs.sdk_playground == 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"
enable-cache: true
working-directory: apps/sdk-playground/python
cache-dependency-glob: uv.lock
- name: Validate SDK Playground Python server
if: steps.sdk-changes.outputs.sdk_playground == 'true'
working-directory: apps/sdk-playground/python
run: |
uv sync --locked --python 3.12
.venv/bin/python -m py_compile server.py
.venv/bin/python -c "import server"
- name: Run Tools unit tests
if: steps.sdk-changes.outputs.tools == 'true'
run: bun run --cwd packages/tools test:unit
- name: Build Tools package
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd packages/tools build
- name: Run AI SDK type checking
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd packages/ai-sdk check-types
- name: Run AI SDK unit tests
if: steps.sdk-changes.outputs.ai_sdk == 'true'
run: bun run --cwd packages/ai-sdk test:unit
- name: Build AI SDK package
if: steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd packages/ai-sdk build
- name: Run SDK Playground type checking
if: steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd apps/sdk-playground check-types:app
- name: Build SDK Playground
if: steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd apps/sdk-playground build:app
- name: Run Memory Graph type checking
run: bun run --cwd packages/memory-graph check-types
- name: Run Memory Graph unit tests
run: bun run --cwd packages/memory-graph test
- name: Run Biome CI (format & lint on changed files)
run: bunx biome ci --changed --since=origin/main --no-errors-on-unmatched

View file

@ -38,6 +38,7 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
allowed_bots: "vorflux[bot]"
# Enable progress tracking
track_progress: true

View file

@ -23,20 +23,22 @@ jobs:
working-directory: ./packages/agent-framework-python
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install build dependencies
run: pip install hatchling build
run: python -m pip install hatchling build
- name: Build package
run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: packages/agent-framework-python/dist/

View file

@ -4,7 +4,7 @@ on:
push:
branches:
- main
paths:
paths:
- "packages/ai-sdk/package.json"
concurrency:
@ -15,7 +15,7 @@ jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
permissions:
contents: read
id-token: write
defaults:
@ -38,26 +38,65 @@ jobs:
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
working-directory: .
run: bun install --frozen-lockfile
- name: Check if version changed
id: version-check
run: |
PACKAGE_NAME=$(jq -r '.name' package.json)
LOCAL_VERSION=$(jq -r '.version' package.json)
NPM_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VERSION" = "$NPM_VERSION" ]; then
if npm view "$PACKAGE_NAME@$LOCAL_VERSION" version >/dev/null 2>&1; then
echo "Version $LOCAL_VERSION already published, skipping."
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "Publishing $LOCAL_VERSION (npm has $NPM_VERSION)"
echo "Publishing $LOCAL_VERSION."
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Build
- name: Wait for the Tools dependency
if: steps.version-check.outputs.changed == 'true'
run: |
TOOLS_SPEC=$(jq -r '.dependencies["@supermemory/tools"]' package.json)
TOOLS_VERSION=${TOOLS_SPEC#^}
TOOLS_VERSION=${TOOLS_VERSION#~}
if [[ ! "$TOOLS_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
echo "Unsupported @supermemory/tools dependency spec: $TOOLS_SPEC" >&2
exit 1
fi
for attempt in {1..20}; do
PUBLISHED_VERSION=$(npm view "@supermemory/tools@$TOOLS_VERSION" version 2>/dev/null || true)
if [ "$PUBLISHED_VERSION" = "$TOOLS_VERSION" ]; then
echo "@supermemory/tools@$TOOLS_VERSION is available on npm."
exit 0
fi
echo "Waiting for @supermemory/tools@$TOOLS_VERSION (attempt $attempt/20)."
sleep 15
done
echo "@supermemory/tools@$TOOLS_VERSION was not published within five minutes." >&2
exit 1
- name: Build Tools dependency
if: steps.version-check.outputs.changed == 'true'
run: bun run --cwd ../tools build
- name: Build AI SDK package
if: steps.version-check.outputs.changed == 'true'
run: bun run build
- name: Verify packed artifact
if: steps.version-check.outputs.changed == 'true'
run: |
npm pack --dry-run --json > "$RUNNER_TEMP/ai-sdk-pack.json"
jq -e '
(.[0].files | any(.path == "dist/index.js")) and
(.[0].files | any(.path == "dist/index.d.ts"))
' "$RUNNER_TEMP/ai-sdk-pack.json" >/dev/null
- name: Publish
if: steps.version-check.outputs.changed == 'true'
run: npm publish --access public --provenance

View file

@ -23,20 +23,22 @@ jobs:
working-directory: ./packages/cartesia-sdk-python
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install build dependencies
run: pip install hatchling build
run: python -m pip install hatchling build
- name: Build package
run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: packages/cartesia-sdk-python/dist/

View file

@ -23,20 +23,22 @@ jobs:
working-directory: ./packages/pipecat-sdk-python
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install build dependencies
run: pip install hatchling build
run: python -m pip install hatchling build
- name: Build package
run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: packages/pipecat-sdk-python/dist/

View file

@ -17,6 +17,7 @@ import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
@ -212,7 +213,9 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
memoryLength: memoryText.length,
})
iconElement.dataset.memoriesData = String(response.data)
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")

View file

@ -17,6 +17,7 @@ import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
@ -459,7 +460,9 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
memoryLength: memoryText.length,
})
iconElement.dataset.memoriesData = String(response.data)
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")

View file

@ -17,6 +17,7 @@ import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
@ -417,7 +418,9 @@ async function getRelatedMemoriesForGemini(actionSource: string) {
if (response?.success && response?.data && input) {
const memoryText = showMemorySuggestion("gemini", input, response.data)
iconElement.dataset.memoriesData = String(response.data)
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
iconElement.dataset.supermemories = memoryText
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")

View file

@ -12,6 +12,46 @@ export function buildSupermemoryText(memories: unknown): string {
return `\n\n${SUPERMEMORY_PREFIX} ${memoryText}`
}
function normalizeMemoryList(memories: unknown): string[] {
const list = Array.isArray(memories)
? memories
: memories == null
? []
: [memories]
return list
.map((memory) => (typeof memory === "string" ? memory : String(memory)))
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0)
}
export function serializeMemoriesForDataset(memories: unknown): string {
const list = normalizeMemoryList(memories)
return list.length > 0 ? JSON.stringify(list) : ""
}
export function parseMemoriesFromDataset(
raw: string | null | undefined,
): string[] {
if (!raw) return []
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return normalizeMemoryList(parsed)
} catch {
// Not JSON — fall through to the legacy delimiter split.
}
return raw
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
}
export function renumberIncludedMemories(memories: string[]): string[] {
return memories.map((memory, index) => {
const text = memory.replace(/^\d+\.\s*/, "").replace(/\s+$/, "")
return `${index + 1}. ${text} \n`
})
}
export function showMemorySuggestion(
platform: string,
input: SuggestionInput,
@ -305,10 +345,7 @@ export function showMarkerPopover(
color: rgba(255, 255, 255, 0.76);
`
memories
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
parseMemoriesFromDataset(memories)
.slice(0, 5)
.forEach((memory) => {
const item = document.createElement("div")

View file

@ -10,11 +10,30 @@ import {
autoCapturePromptsEnabled,
} from "../../utils/storage"
import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components"
import {
buildSupermemoryText,
parseMemoriesFromDataset,
renumberIncludedMemories,
serializeMemoriesForDataset,
} from "./memory-suggestion"
let t3DebounceTimeout: NodeJS.Timeout | null = null
let t3RouteObserver: MutationObserver | null = null
let t3UrlCheckInterval: NodeJS.Timeout | null = null
let t3ObserverThrottle: NodeJS.Timeout | null = null
let t3IncludedPopup: {
el: HTMLElement
onClick: (event: MouseEvent) => void
timer: ReturnType<typeof setTimeout>
} | null = null
function disposeT3IncludedPopup() {
if (!t3IncludedPopup) return
document.removeEventListener("click", t3IncludedPopup.onClick)
clearTimeout(t3IncludedPopup.timer)
t3IncludedPopup.el.remove()
t3IncludedPopup = null
}
export function initializeT3() {
if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
@ -53,6 +72,7 @@ function setupT3RouteChangeDetection() {
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
disposeT3IncludedPopup()
currentUrl = window.location.href
setTimeout(() => {
addSupermemoryIconToT3Input()
@ -231,9 +251,13 @@ async function getRelatedMemoriesForT3(actionSource: string) {
}
if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
textareaElement.dataset.supermemories = buildSupermemoryText(
response.data,
)
iconElement.dataset.memoriesData = response.data
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
updateT3IconFeedback("Included Memories", iconElement)
} else {
@ -268,6 +292,8 @@ function updateT3IconFeedback(
iconElement.dataset.originalHtml = iconElement.innerHTML
}
disposeT3IncludedPopup()
const feedbackDiv = document.createElement("div")
feedbackDiv.style.cssText = `
display: flex;
@ -329,11 +355,9 @@ function updateT3IconFeedback(
overflow-y: auto;
`
const memoriesText = iconElement.dataset.memoriesData || ""
const individualMemories = memoriesText
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
const individualMemories = parseMemoriesFromDataset(
iconElement.dataset.memoriesData,
)
individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div")
@ -405,66 +429,65 @@ function updateT3IconFeedback(
popup.style.display = "block"
})
document.addEventListener("click", (e) => {
const onClick = (e: MouseEvent) => {
if (!popup.contains(e.target as Node)) {
popup.style.display = "none"
}
})
}
document.addEventListener("click", onClick)
t3IncludedPopup = {
el: popup,
onClick,
timer: setTimeout(disposeT3IncludedPopup, 300000),
}
content.querySelectorAll("button[data-memory-index]").forEach((button) => {
const htmlButton = button as HTMLButtonElement
htmlButton.addEventListener("click", () => {
const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
const memoryItem = htmlButton.parentElement
htmlButton.parentElement?.remove()
if (memoryItem) {
content.removeChild(memoryItem)
}
const currentMemories = (iconElement.dataset.memoriesData || "")
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
currentMemories.splice(index, 1)
const updatedMemories = currentMemories.join(" ,")
iconElement.dataset.memoriesData = updatedMemories
const remainingMemories = parseMemoriesFromDataset(
iconElement.dataset.memoriesData,
)
remainingMemories.splice(index, 1)
const remaining = renumberIncludedMemories(remainingMemories)
const textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
// Only wipe when nothing remains — `<= 1` used to discard the last kept memory.
if (remaining.length === 0) {
if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
delete iconElement.dataset.memoriesData
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
disposeT3IncludedPopup()
return
}
iconElement.dataset.memoriesData =
serializeMemoriesForDataset(remaining)
if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}`
textareaElement.dataset.supermemories =
buildSupermemoryText(remaining)
}
content
.querySelectorAll("button[data-memory-index]")
.forEach((btn, newIndex) => {
const htmlBtn = btn as HTMLButtonElement
htmlBtn.dataset.memoryIndex = newIndex.toString()
htmlBtn.dataset.memoryIndex = String(newIndex)
const label = htmlBtn.previousElementSibling
if (label) {
label.textContent = remaining[newIndex].trim()
}
})
if (currentMemories.length <= 1) {
if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories
delete iconElement.dataset.memoriesData
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}
popup.style.display = "none"
if (document.body.contains(popup)) {
document.body.removeChild(popup)
}
}
})
})
setTimeout(() => {
if (document.body.contains(popup)) {
document.body.removeChild(popup)
}
}, 300000)
}
iconElement.innerHTML = ""
@ -556,6 +579,7 @@ function setupT3PromptCapture() {
if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
disposeT3IncludedPopup()
}
const handleT3SendButtonClick = async (event: Event) => {
@ -711,6 +735,7 @@ async function setupT3AutoFetch() {
if (textareaElement.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
disposeT3IncludedPopup()
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
}

View file

@ -9,9 +9,9 @@
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"check-types": "wxt prepare && tsc --noEmit",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
},
"dependencies": {

View file

@ -14,10 +14,12 @@ Company Brain doesn't only answer when you @mention it. It can run recurring wor
An automation is a prompt that runs on a schedule and posts the result somewhere. You write it once, in plain language:
<SlackThread channel="#product">
<SlackMessage self time="9:03 AM">
<SlackMessage self hasAvatar time="9:03 AM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> every Monday at 9am, post a digest of what shipped last week and what's still open, to <ChannelRef>product</ChannelRef>.
</SlackMessage>
<SlackMessage bot time="9:03 AM">
<SlackMessage bot hasAvatar time="9:03 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Got it — scheduled. First digest posts Monday, 9:00 AM, to <ChannelRef>product</ChannelRef>.
</SlackMessage>
</SlackThread>

View file

@ -25,10 +25,12 @@ It'll answer a question, actually go do the work, or just jump into a conversati
### Ask it anything
<SlackThread channel="#product">
<SlackMessage self time="10:42 AM">
<SlackMessage self hasAvatar time="10:42 AM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> what did we decide about pricing for the company brain
</SlackMessage>
<SlackMessage bot time="10:42 AM">
<SlackMessage bot hasAvatar time="10:42 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
You and mahesh decided yesterday that it stays at **$400/mo**, usage based only. we landed on that in a <ChannelRef>product</ChannelRef> huddle
</SlackMessage>
</SlackThread>
@ -36,10 +38,12 @@ It'll answer a question, actually go do the work, or just jump into a conversati
### Put it to work
<SlackThread channel="#eng">
<SlackMessage self time="3:24 PM">
<SlackMessage self hasAvatar time="3:24 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> is the Stripe webhook timeout from Sentry already tracked anywhere?
</SlackMessage>
<SlackMessage bot time="3:25 PM">
<SlackMessage bot hasAvatar time="3:25 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
nope, nothing yet — just opened **ENG-847** for it, added the sentry issue to it, and put it on <Mention>Sam</Mention> since they were the last one in `webhooks/stripe.ts` and their beautiful code broke it ☠️. <AgentLink href="https://linear.app">here you go</AgentLink>, should probably fix it asap.
</SlackMessage>
</SlackThread>
@ -50,7 +54,8 @@ It'll answer a question, actually go do the work, or just jump into a conversati
<SlackMessage name="Alex" color="#E01E5A" time="11:03 AM">
is prod down? a couple of customers are pinging me
</SlackMessage>
<SlackMessage bot time="11:03 AM">
<SlackMessage bot hasAvatar time="11:03 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
not fully down — `api/search` is just elevated, 42 errors in the last 15 min (SM-2041), and <Mention>Kush</Mention> is on it. probably that deploy from this morning. Only one user has complained on support and i already replied to them saying it's being investigated.
</SlackMessage>
</SlackThread>

View file

@ -16,10 +16,12 @@ You don't just want to *know* something — you want Company Brain to do the nex
### Read from a tool
<SlackThread channel="#eng">
<SlackMessage self time="3:22 PM">
<SlackMessage self hasAvatar time="3:22 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> what are my open PRs in the web repo?
</SlackMessage>
<SlackMessage bot time="3:22 PM">
<SlackMessage bot hasAvatar time="3:22 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
You have **3 open PRs** on `supermemory/web`:
- `#1199` staff custom MCP connection cards — review requested
- `#1197` company brain onboarding research UI — changes requested
@ -30,10 +32,12 @@ You don't just want to *know* something — you want Company Brain to do the nex
### Write to a tool
<SlackThread channel="#eng">
<SlackMessage self time="3:24 PM">
<SlackMessage self hasAvatar time="3:24 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> create a Linear issue: fix the onboarding redirect
</SlackMessage>
<SlackMessage bot time="3:24 PM">
<SlackMessage bot hasAvatar time="3:24 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Created **ENG-842** — *Fix the onboarding redirect* in Engineering, assigned to you. <AgentLink href="https://linear.app">Open in Linear</AgentLink>
</SlackMessage>
</SlackThread>

View file

@ -5,7 +5,7 @@ description: "Connect card, welcome DM, and first answer — activation on day o
icon: "user-plus"
---
import { SlackThread, SlackMessage, BOT_AVATAR } from "/snippets/slack-message.mdx";
import { SlackThread, SlackMessage } from "/snippets/slack-message.mdx";
## The scenario
@ -15,8 +15,10 @@ A new hire joins the Slack workspace. They shouldn't need a web signup form or a
They get a connect card, tap **Connect me**, and receive a welcome DM:
<SlackThread type="dm" dmWith={{ name: "supermemory", avatar: BOT_AVATAR }}>
<SlackMessage bot time="9:02 AM">
<SlackThread type="dm" dmWith={{ name: "supermemory" }} hasAvatar>
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
<SlackMessage bot hasAvatar time="9:02 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Welcome to **Acme**. Here's what I know, what I can access, and what I keep private.
Try one of these:
@ -27,7 +29,8 @@ They get a connect card, tap **Connect me**, and receive a welcome DM:
<SlackMessage name="Sam" color="#36C5F0" time="9:03 AM">
What does Acme do?
</SlackMessage>
<SlackMessage bot time="9:03 AM">
<SlackMessage bot hasAvatar time="9:03 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Acme builds memory infrastructure for AI apps — shared context for teams and agents. *(from the company profile your admin seeded at setup)*
</SlackMessage>
</SlackThread>

View file

@ -17,7 +17,8 @@ Someone drops a vague "is prod down?" in `#eng`. Company Brain correlates it wit
<SlackMessage name="Alex" color="#E01E5A" time="11:03 AM">
is prod down? a couple of customers are pinging me
</SlackMessage>
<SlackMessage bot time="11:03 AM">
<SlackMessage bot hasAvatar time="11:03 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Not a full outage — **`api/search` is elevated** (42 errors in the last 15m, mostly `timeout on vectorize`). Sentry issue <AgentLink href="https://example.com">SM-2041</AgentLink>. Deploy from this morning is the likely culprit; rollback candidate is `af61880`.
</SlackMessage>
</SlackThread>
@ -25,7 +26,8 @@ Someone drops a vague "is prod down?" in `#eng`. Company Brain correlates it wit
Or, without anyone asking, a scheduled automation posts every morning:
<SlackThread channel="#eng">
<SlackMessage bot time="9:00 AM">
<SlackMessage bot hasAvatar time="9:00 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
**Daily error digest** — 3 issues new since yesterday, 1 still spiking (`api/search` timeout). Full list in Sentry.
</SlackMessage>
</SlackThread>

View file

@ -14,10 +14,12 @@ The Q2 roadmap lives in Notion. Nobody wants to dig for the link mid-conversatio
## What happens
<SlackThread channel="#product">
<SlackMessage self time="10:18 AM">
<SlackMessage self hasAvatar time="10:18 AM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> what's in our Q2 roadmap?
</SlackMessage>
<SlackMessage bot time="10:18 AM">
<SlackMessage bot hasAvatar time="10:18 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Three themes for Q2:
1. **Company Brain GA** — Slack bot, connectors, automations
2. **Memory Router v2** — lower latency, better multi-tenant isolation

View file

@ -14,10 +14,12 @@ Some questions aren't a quick lookup. They need pulling information from a lot o
## What it will look like
<SlackThread channel="#product">
<SlackMessage self time="11:10 AM">
<SlackMessage self hasAvatar time="11:10 AM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> prepare a brief on how we've handled enterprise SSO requests over the last two quarters — who asked, what we promised, what's still open, and what engineering thinks the real effort is
</SlackMessage>
<SlackMessage bot time="11:14 AM">
<SlackMessage bot hasAvatar time="11:14 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
**Enterprise SSO — two-quarter brief**
- **Demand:** 6 serious asks (Acme, Globex, Initech, …). Three converted to paid pilots.

View file

@ -14,10 +14,12 @@ Someone missed the Acme call — or was there and forgot the outcome. They ask i
## What happens
<SlackThread channel="#sales">
<SlackMessage self time="4:05 PM">
<SlackMessage self hasAvatar time="4:05 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> what did we decide in the meeting with Acme?
</SlackMessage>
<SlackMessage bot time="4:05 PM">
<SlackMessage bot hasAvatar time="4:05 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Acme is moving to annual billing at the current rate, with a 90-day pilot on the enterprise SSO add-on. Follow-up owned by **Priya**, due Friday. *(from Granola notes, Acme QBR — Jun 11)*
</SlackMessage>
</SlackThread>

View file

@ -21,7 +21,8 @@ A teammate needs help and doesn't want to play calendar ping-pong. They ask in S
<SlackMessage name="Sam" color="#36C5F0" time="1:12 PM">
Dhravya are you free this afternoon? need some help with dev setup
</SlackMessage>
<SlackMessage bot time="1:12 PM">
<SlackMessage bot hasAvatar time="1:12 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Dhravya is free at **2:00 PM**. Sending an invite to block **20 mins** on both your calendars.
</SlackMessage>
</SlackThread>

View file

@ -14,10 +14,12 @@ A test is failing and the stack trace alone isn't enough. You want Company Brain
## What happens
<SlackThread channel="#eng">
<SlackMessage self time="5:41 PM">
<SlackMessage self hasAvatar time="5:41 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> the `permissions.lease` unit test is failing on main — can you reproduce and tell me why?
</SlackMessage>
<SlackMessage bot time="5:42 PM">
<SlackMessage bot hasAvatar time="5:42 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Reproduced in a sandbox against `main`.
**Failure:** `permissions.lease` expects temporary access to expire in 15 minutes, but the fixture still asserts 30.

View file

@ -14,7 +14,13 @@ A customer files a ticket through Plain. It lands in `#support`, gets triaged wi
## What happens
<SlackThread channel="#support" members={24}>
<SlackMessage name="Plain" badges={["APP"]} avatar="/images/company-brain/plain-icon.png" time="10:12 AM">
<SlackMessage
name="Plain"
badges={["APP"]}
hasAvatar
time="10:12 AM"
>
<img src="/images/company-brain/plain-icon.png" alt="" />
New conversation: <AgentLink href="#">rewriteQuery param not working</AgentLink>
<br />
**Jordan Alvarez** (acme-corp.io) sent a **new message**.
@ -22,7 +28,8 @@ A customer files a ticket through Plain. It lands in `#support`, gets triaged wi
hi team, just tried the `rewriteQuery` param on the v3 search endpoint and it doesn't seem to actually do anything — tried a few different values, results look identical either way. can someone take a look
</SlackUnfurl>
</SlackMessage>
<SlackMessage bot time="10:13 AM">
<SlackMessage bot hasAvatar time="10:13 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Confirmed, this is a real one — a couple of people have also flagged it on GitHub over the last week.
<br />
<br />
@ -34,12 +41,24 @@ A customer files a ticket through Plain. It lands in `#support`, gets triaged wi
<br />
<FileAttachment name="Context.md" />
</SlackMessage>
<SlackMessage name="Cursor" badges={["AGENT"]} avatar="/images/company-brain/cursor-icon.png" time="10:14 AM">
<SlackMessage
name="Cursor"
badges={["AGENT"]}
hasAvatar
time="10:14 AM"
>
<img src="/images/company-brain/cursor-icon.png" alt="" />
<AgentLink href="#">Agent thread started</AgentLink>
<br />
Reproducing against the v3 search test suite now.
</SlackMessage>
<SlackMessage name="Cursor" badges={["AGENT"]} avatar="/images/company-brain/cursor-icon.png" time="10:19 AM">
<SlackMessage
name="Cursor"
badges={["AGENT"]}
hasAvatar
time="10:19 AM"
>
<img src="/images/company-brain/cursor-icon.png" alt="" />
Fixed — `rewriteQuery` was getting skipped by the new cost short-circuit whenever a query was already cached. Pushed on <AgentLink href="#">#2312</AgentLink>.
<br />
<br />
@ -56,7 +75,8 @@ A customer files a ticket through Plain. It lands in `#support`, gets triaged wi
<SlackButton variant="primary">Open in Web</SlackButton>
<SlackButton>Open in Desktop</SlackButton>
</SlackMessage>
<SlackMessage bot time="10:20 AM">
<SlackMessage bot hasAvatar time="10:20 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
<Mention self>Dhravya</Mention> I'll let you review that and let the customer know we have a fix ready.
<br />
Please do it ASAP — it's an enterprise customer!

View file

@ -17,7 +17,8 @@ A customer question lands in `#support`. Nobody has to @mention the bot — it a
<SlackMessage name="Maya" color="#2BAC76" time="2:14 PM">
customer on the Pro plan is asking if they can export their full memory graph as CSV — do we support that?
</SlackMessage>
<SlackMessage bot time="2:14 PM">
<SlackMessage bot hasAvatar time="2:14 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Yes — **Settings → Export → Full graph (CSV)**. Available on Pro and above. Same answer went out on ticket PLN-1842 last week if you want the exact wording.
</SlackMessage>
</SlackThread>

View file

@ -56,7 +56,7 @@ Uploading a long PDF does more than store bytes: Supermemory derives many memori
## Properties and rules of memories
1. Memories are atomic - Each memory has enough information and context about one particular topic
2. They always build on top of each other - with `updates`, the model knows the history, a memory `extends` from other memories, and new facts are derived (`derives` relation) from existing knowledg.e
2. They always build on top of each other - with `updates`, the model knows the history, a memory `extends` from other memories, and new facts are derived (`derives` relation) from existing knowledge.
## Memory relationships

View file

@ -194,6 +194,11 @@
{
"group": "Other resources",
"pages": [
{
"group": "General",
"icon": "book-open",
"pages": ["ingestion/batch-ingest-historical-data"]
},
{
"group": "Benchmarking",
"icon": "flask-conical",
@ -370,7 +375,10 @@
{
"group": "Setups",
"icon": "layers",
"pages": ["supermemory-mcp/claude-desktop"]
"pages": [
"supermemory-mcp/chatgpt-web",
"supermemory-mcp/claude-desktop"
]
}
]
},
@ -380,6 +388,8 @@
"pages": [
"integrations/openclaw",
"integrations/claude-code",
"integrations/cursor",
"integrations/grok-bot",
"integrations/opencode",
"integrations/codex",
"integrations/hermes"

View file

@ -0,0 +1 @@
<svg fill="none" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m96 73h320.735v365.65h-320.735z"/></clipPath><path d="m512.001 325.499c0 7.124 0 14.24-.041 21.364-.035 5.999-.103 11.998-.268 17.99-.356 13.068-1.124 26.245-3.448 39.169-2.359 13.109-6.205 25.306-12.266 37.222-5.958 11.703-13.746 22.419-23.029 31.709-9.29 9.291-20 17.072-31.71 23.03-11.909 6.061-24.113 9.907-37.222 12.266-12.923 2.324-26.101 3.092-39.169 3.448-5.999.165-11.991.233-17.99.268-7.123.048-14.24.041-21.364.041h-139c-7.124 0-14.24 0-21.364-.041-5.999-.035-11.998-.103-17.99-.268-13.068-.356-26.245-1.124-39.169-3.448-13.109-2.359-25.306-6.205-37.2219-12.266-11.7034-5.958-22.4195-13.746-31.7095-23.03-9.29-9.29-17.0717-19.999-23.0296-31.709-6.06082-11.909-9.90709-24.113-12.26559-37.222-2.32422-12.924-3.092104-26.101-3.448621-39.169-.164547-5.999-.2331075-11.991-.267388-17.99-.02742444-7.124-.02742444-14.24-.02742444-21.364v-139c0-7.124 0-14.24.04113664-21.364.0342805-5.999.1028418-11.998.2673878-17.99.356517-13.068 1.124399-26.245 3.448619-39.169 2.3585-13.1091 6.20477-25.3061 12.26558-37.222 5.9579-11.7034 13.7465-22.4195 23.0296-31.7095 9.2901-9.29 19.9993-17.0717 31.7095-23.0296 11.9091-6.06082 24.1129-9.90709 37.2222-12.26559 12.923-2.32422 26.101-3.092101 39.168-3.448618 6-.164547 11.992-.2331075 17.991-.2673881 7.117-.03428046 14.233-.03428046 21.357-.03428046h139c7.124 0 14.24 0 21.364.04113656 5.999.0342806 11.998.102842 17.99.267388 13.068.356517 26.245 1.124402 39.169 3.448622 13.109 2.3585 25.306 6.20477 37.222 12.26553 11.703 5.958 22.419 13.7465 31.709 23.0297 9.29 9.29 17.072 19.9992 23.03 31.7094 6.061 11.9091 9.907 24.113 12.266 37.2222 2.324 12.923 3.092 26.101 3.448 39.169.165 5.999.233 11.991.268 17.99.048 7.123.041 14.24.041 21.364v139z" fill="#14120b"/><path d="m186.501 3.99902h139c7.125 0 14.231-.00004 21.341.04102 5.985.0342 11.952.10221 17.903.26562h.001c13.003.35475 25.943 1.11709 38.569 3.3877 12.775 2.29828 24.592 6.03144 36.118 11.89354v-.001c10.973 5.5867 21.052 12.8384 29.848 21.4571l.847.8379c8.993 8.993 16.525 19.3593 22.292 30.6933l3.565-1.8135-3.565 1.8145c5.678 11.1575 9.36 22.6024 11.674 34.9208l.219 1.195c2.129 11.837 2.932 23.95 3.316 36.132l.072 2.438c.164 5.958.232 11.919.266 17.904v.004c.048 7.107.041 14.207.041 21.337v129.344l-.007-.007v9.656c0 7.125 0 14.231-.041 21.341-.034 5.985-.102 11.952-.266 17.903v.001c-.354 13.003-1.117 25.944-3.387 38.569-2.227 12.376-5.8 23.853-11.351 35.037l-.543 1.08c-5.767 11.327-13.306 21.701-22.293 30.695-8.993 8.993-19.361 16.526-30.695 22.293-11.518 5.861-23.342 9.595-36.116 11.894-11.837 2.128-23.95 2.931-36.132 3.315l-2.438.072c-5.958.164-11.919.232-17.904.266h-.004c-7.107.048-14.207.041-21.337.041h-139c-7.125 0-14.231 0-21.341-.041-5.985-.034-11.952-.102-17.903-.266h-.001c-13.003-.355-25.944-1.117-38.57-3.387-12.7742-2.299-24.5914-6.032-36.1165-11.894h.001c-10.974-5.587-21.0534-12.837-29.8496-21.456l-.8467-.838c-8.7118-8.712-16.0531-18.713-21.7461-29.634l-.5459-1.06-.544-1.081c-5.3709-10.816-8.8904-21.919-11.12983-33.84l-.21973-1.196c-2.27061-12.625-3.03294-25.566-3.38769-38.569v-.001c-.16342-5.958-.23143-11.918-.26563-17.903-.02736-7.112-.02734-14.219-.02734-21.341v-139c0-7.125-.00005-14.231.04101-21.341.0342-5.985.10222-11.952.26563-17.903v-.001c.35475-13.003 1.11699-25.944 3.38769-38.57 2.29829-12.7743 6.03159-24.5916 11.89359-36.1166l-.001-.001c5.7665-11.3269 13.3073-21.6999 22.2939-30.6934 8.9933-8.9932 19.36-16.5261 30.6944-22.2929h.0009c11.5174-5.8615 23.3407-9.59617 36.1149-11.89455 11.837-2.12878 23.951-2.93017 36.133-3.31446l2.438-.07226c5.958-.16342 11.919-.23142 17.904-.26563l-.001-.00097c7.104-.03422 14.211-.03321 21.335-.03321z" stroke="#edecec" stroke-opacity=".2" stroke-width="8"/><g clip-path="url(#a)"><path d="m410.344 159.545-146.38-84.5111c-4.7-2.7145-10.5-2.7145-15.2 0l-146.373 84.5111c-3.9515 2.282-6.391 6.501-6.391 11.071v170.418c0 4.569 2.4395 8.789 6.391 11.07l146.379 84.512c4.701 2.714 10.501 2.714 15.201 0l146.38-84.512c3.951-2.281 6.391-6.501 6.391-11.07v-170.418c0-4.57-2.44-8.789-6.391-11.071zm-9.195 17.902-141.308 244.751c-.955 1.65-3.477.976-3.477-.934v-160.261c0-3.203-1.711-6.164-4.487-7.772l-138.786-80.127c-1.65-.956-.976-3.478.934-3.478h282.616c4.013 0 6.522 4.35 4.515 7.828h-.007z" fill="#edecec"/></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View file

@ -4,18 +4,14 @@ description: "Context infrastructure for AI agents"
mode: "custom"
---
export const HeroCard = ({ imageUrl, title, description, href }) => {
export const HeroCard = ({ title, description, href, children }) => {
return (
<a
className="group flex flex-col h-full rounded-2xl overflow-hidden bg-white dark:bg-zinc-950 border border-gray-200/80 dark:border-zinc-800 hover:border-gray-300 dark:hover:border-zinc-700 hover:shadow-md transition-all duration-300"
href={href}
>
<div className="overflow-hidden bg-gray-50 dark:bg-zinc-900 border-b border-gray-100 dark:border-zinc-800">
<img
src={imageUrl}
className="w-full h-56 sm:h-64 object-cover transform group-hover:scale-[1.03] transition-transform duration-500"
alt={title}
/>
{children}
</div>
<div className="flex flex-col gap-2 p-6 sm:p-7">
<h3 className="text-lg sm:text-xl font-semibold text-gray-900 dark:text-zinc-50 tracking-tight">
@ -62,29 +58,49 @@ export const HeroCard = ({ imageUrl, title, description, href }) => {
<div className="mt-12 sm:mt-14 grid sm:grid-cols-2 gap-5 sm:gap-6">
<HeroCard
imageUrl="/images/intro-company-brain.jpg"
title="Developer platform"
description="API reference, concepts, and SDKs"
href="/overview/what-is-supermemory"
/>
>
<img
src="/images/intro-company-brain.jpg"
className="w-full h-56 sm:h-64 object-cover transform group-hover:scale-[1.03] transition-transform duration-500"
alt="Developer platform"
/>
</HeroCard>
<HeroCard
imageUrl="/images/intro-company-brain-card.jpg"
title="Company brain"
description="Team knowledge on the same engine"
href="/company-brain/overview"
/>
>
<img
src="/images/intro-company-brain-card.jpg"
className="w-full h-56 sm:h-64 object-cover transform group-hover:scale-[1.03] transition-transform duration-500"
alt="Company brain"
/>
</HeroCard>
<HeroCard
imageUrl="/images/intro-plugins.jpg"
title="Plugins and MCP"
description="Drop memory into Claude and coding agents"
href="/supermemory-mcp/mcp"
/>
>
<img
src="/images/intro-plugins.jpg"
className="w-full h-56 sm:h-64 object-cover transform group-hover:scale-[1.03] transition-transform duration-500"
alt="Plugins and MCP"
/>
</HeroCard>
<HeroCard
imageUrl="/images/intro-developer-platform.png"
title="Self-hosting"
description="Run it locally with zero config"
href="/self-hosting/overview"
/>
>
<img
src="/images/intro-developer-platform.png"
className="w-full h-56 sm:h-64 object-cover transform group-hover:scale-[1.03] transition-transform duration-500"
alt="Self-hosting"
/>
</HeroCard>
</div>
</div>
</div>

View file

@ -496,6 +496,7 @@ console.log(doc.status); // "queued" | "processing" | "done"
## Next Steps
- [How to backfill historical data](/ingestion/batch-ingest-historical-data) — Import dated content with the batch API
- [Search Memories](/recall/search) — Query your content
- [User Profiles](/recall/user-profiles) — Get user context
- [Organizing & Filtering](/concepts/filtering) — Container tags and metadata

View file

@ -0,0 +1,145 @@
---
title: "How to backfill historical data into Supermemory"
sidebarTitle: "Backfill historical data"
description: "Backfill historical documents into Supermemory with documentDate, stable custom IDs, and the batch ingestion API."
icon: "history"
---
Use `POST /v3/documents/batch` to backfill exports, emails, messages, or other dated records.
<Warning>
Sort the source data oldest to newest, add `documentDate` to every document.
</Warning>
## Backfill in batches
Backfill dated content by setting `documentDate` on each document, sorting the source records oldest to newest, and sending them in batches. Each request can contain up to 600 documents.
**Endpoint:** [`POST /v3/documents/batch`](/api-reference/ingest/batch-add-documents)
<CodeGroup>
```typescript TypeScript
import Supermemory from "supermemory";
type SourceDocument = {
id: string;
content: string;
createdAt: string;
};
const client = new Supermemory();
const batchSize = 100;
async function backfillHistoricalData(sourceDocuments: SourceDocument[]) {
const documents = sourceDocuments
.map((document) => ({
content: document.content,
customId: document.id,
documentDate: new Date(document.createdAt).toISOString()
}))
.sort((a, b) => a.documentDate.localeCompare(b.documentDate));
for (let offset = 0; offset < documents.length; offset += batchSize) {
const result = await client.documents.batchAdd({
containerTag: "historical_import",
documents: documents.slice(offset, offset + batchSize)
});
if (result.failed > 0) {
throw new Error(`${result.failed} documents failed to ingest`);
}
}
}
```
```python Python
from datetime import datetime, timezone
from supermemory import Supermemory
client = Supermemory()
batch_size = 100
def to_utc(value: str) -> str:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise ValueError("created_at must include a timezone")
return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def backfill_historical_data(source_documents: list[dict[str, str]]) -> None:
documents = sorted(
[
{
"content": document["content"],
"custom_id": document["id"],
"document_date": to_utc(document["created_at"]),
}
for document in source_documents
],
key=lambda document: document["document_date"],
)
for offset in range(0, len(documents), batch_size):
result = client.documents.batch_add(
container_tag="historical_import",
documents=documents[offset : offset + batch_size],
)
if result.failed > 0:
raise RuntimeError(f"{result.failed} documents failed to ingest")
```
</CodeGroup>
## Optional: wait for processing to finish
**Endpoint:** [`GET /v3/documents/{id}`](/api-reference/documents/get-document)
The batch endpoint returns after accepting the documents. If a later step depends on completed memory generation, poll the returned document IDs until both `status` and `dreamingStatus` are `done`.
<CodeGroup>
```typescript TypeScript
async function waitUntilDone(ids: string[]) {
while (true) {
const documents = await Promise.all(
ids.map((id) => client.documents.get(id))
);
if (documents.some((document) => document.status === "failed")) {
throw new Error("A document failed to process");
}
if (
documents.every(
(document) =>
document.status === "done" && document.dreamingStatus === "done"
)
) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 10_000));
}
}
```
```python Python
import time
def wait_until_done(ids: list[str]) -> None:
while True:
documents = [client.documents.get(document_id) for document_id in ids]
if any(document.status == "failed" for document in documents):
raise RuntimeError("A document failed to process")
if all(
document.status == "done" and document.dreaming_status == "done"
for document in documents
):
return
time.sleep(10)
```
</CodeGroup>

View file

@ -145,7 +145,7 @@ Per-repo overrides in `.claude/.supermemory-claude/config.json`. Run `/supermemo
Source code, issues, and detailed README.
</Card>
<Card title="OpenClaw Plugin" icon="messages-square" href="/integrations/openclaw">
Multi-platform memory for Telegram, WhatsApp, Discord, and more.
<Card title="Cursor Plugin" icon="/images/cursor-logo.svg" href="/integrations/cursor">
Memory for your Cursor chats.
</Card>
</CardGroup>

View file

@ -170,7 +170,7 @@ tail -f ~/.codex-supermemory.log
Source code, issues, and detailed README.
</Card>
<Card title="Claude Code Plugin" icon="code" href="/integrations/claude-code">
Memory plugin for Claude Code.
<Card title="Cursor Plugin" icon="/images/cursor-logo.svg" href="/integrations/cursor">
Memory for your Cursor chats.
</Card>
</CardGroup>

View file

@ -0,0 +1,208 @@
---
title: "Cursor"
sidebarTitle: "Cursor"
description: "cursor-supermemory: persistent memory across your Cursor chats"
icon: "/images/cursor-logo.svg"
---
Your agent remembers the decisions, bugs, and conventions from earlier chats instead of starting cold every time.
## Install
<Note>
Requires [Node.js](https://nodejs.org) on your `PATH`. Installing Cursor does not put one there.
</Note>
Run this in Cursor:
```
/add-plugin cursor-supermemory
```
Or install it from the [Cursor Marketplace](https://cursor.com/marketplace/supermemory): open **Customize**, find **Supermemory**, select **Install**, and choose **project** or **user** scope.
Restart Cursor or run **Developer: Reload Window** afterwards.
## Authenticate
Open a new chat in Cursor and run:
```
/supermemory-setup
```
A browser window opens. Sign in to Supermemory and you are done.
As a fallback, set an API key from [API Keys](https://console.supermemory.ai/keys):
<Tabs>
<Tab title="macOS / Linux (zsh)">
```bash
echo 'export SUPERMEMORY_API_KEY="sm_..."' >> ~/.zshrc
source ~/.zshrc
```
</Tab>
<Tab title="macOS / Linux (bash)">
```bash
echo 'export SUPERMEMORY_API_KEY="sm_..."' >> ~/.bashrc
source ~/.bashrc
```
</Tab>
<Tab title="Windows (PowerShell)">
```powershell
[System.Environment]::SetEnvironmentVariable("SUPERMEMORY_API_KEY", "sm_...", "User")
```
Restart your terminal after running this.
</Tab>
</Tabs>
Restart Cursor after installing the plugin or changing credentials.
Check the connection any time with `/supermemory-status`.
<Accordion title="Prefer the terminal?" icon="terminal">
The slash commands just run the plugin's CLI for you. To drive it yourself:
```bash
node "${CURSOR_PLUGIN_ROOT}/dist/cli.js" login
node "${CURSOR_PLUGIN_ROOT}/dist/cli.js" status
node "${CURSOR_PLUGIN_ROOT}/dist/cli.js" logout
```
`CURSOR_PLUGIN_ROOT` is set for plugin hooks. If it is empty in your shell, run `node dist/cli.js <command>` from the installed plugin directory.
Credentials are stored in `~/.supermemory-cursor/credentials.json`.
</Accordion>
## How It Works
| Layer | What it does |
|-------|--------------|
| Session profile | Loads your persistent profile when a Cursor conversation starts |
| Automatic recall | Searches on substantive prompts, deduplicates results, and injects them after the first supported tool result |
| Incremental capture | Saves each completed turn, and retries unsaved transcript deltas at session end |
| MCP tools | Explicit memory control from any Cursor AI session |
| Context gatherer | Fans out targeted searches before substantial work |
| Always-on rule | Makes the agent recall relevant history proactively |
### Skills and Commands
| Name | Type | Description |
|------|------|-------------|
| `memory-init` | Skill | Explore the codebase and initialize project memory |
| `memory-save` | Skill | Save an insight, decision, or solution worth keeping |
| `memory-search` | Skill | Search memory for past work, bugs, and decisions |
| `supermemory-context-gatherer` | Agent | Gather deep background before substantial work |
| `supermemory-setup` | Command | Connect Supermemory to Cursor |
| `supermemory-status` | Command | Check authentication and live connectivity |
| `supermemory-config` | Command | Create or edit the project config file |
| `supermemory-logout` | Command | Disconnect Supermemory from Cursor |
## MCP Tools
| Tool | Description |
|------|-------------|
| `supermemory_get_config` | Show current config, resolved container tags, and config file paths |
| `supermemory_set_config` | Update config at project or global scope |
| `supermemory_containers` | Show what `user` and `project` container tags resolve to |
| `supermemory_search` | Search memories by query |
| `supermemory_add` | Save new information to memory |
| `supermemory_list` | List stored memories |
| `supermemory_forget` | Delete a memory by id or content |
| `supermemory_profile` | Get your user profile summary |
Every tool that takes a `container` argument accepts:
- `"user"` (default): personal memories for the current repository
- `"project"`: project knowledge for the current repository
- `"both"`: both scopes plus compatible legacy memories
- any custom string: used as a raw container tag
`user` and `project` write to the same repository container. The `sm_scope` metadata field is what keeps personal and session memories separate from explicit project knowledge when an agent asks for one scope.
## Container Tags
Cursor shares one repository tag with the [Claude Code](/integrations/claude-code), [OpenAI Codex](/integrations/codex), and [OpenCode](/integrations/opencode) plugins, so agents working on the same repo read and write the same memory:
```text
repo_<project_name>__<project_id>
```
The project ID is a stable hash of the normalized Git remote. Repositories without a remote fall back to their resolved local path. Two repos with the same directory name never collide, and different agents on the same repository share memory.
The plugin still reads the former `cursor_user_*` and `cursor_project_*` tags, along with legacy tags from the other agents. New writes only use the unified repository tag. Set `repoContainerTag` only when you need an explicit shared override.
## Configuration
<Tip>
**Prefer to keep everything on your machine?** This plugin works with [self-hosted Supermemory](/self-hosting/overview): run `npx supermemory local`, then set `SUPERMEMORY_API_URL="http://localhost:6767"` (or `baseUrl` in your config file) and use the API key printed on first boot.
</Tip>
### Environment variables
| Variable | Description |
|----------|-------------|
| `SUPERMEMORY_API_KEY` | API key (overrides all other sources) |
| `SUPERMEMORY_API_URL` | Override the Supermemory API base URL |
| `SUPERMEMORY_REPO_TAG` | Override the unified repository container tag |
| `SUPERMEMORY_USER_TAG` | Legacy Cursor personal container to continue reading |
| `SUPERMEMORY_PROJECT_TAG` | Legacy Cursor project container to continue reading |
| `CURSOR_USER_EMAIL` | Used only to find legacy Cursor personal memories |
### Global config
`~/.config/cursor/supermemory.json` holds user-wide defaults and applies to all projects.
```json
{
"repoContainerTag": "repo_my_project__0123456789abcdef",
"similarityThreshold": 0.55,
"maxMemories": 10,
"injectProfile": true,
"signalExtraction": false,
"signalKeywords": ["remember", "architecture", "decision", "bug", "fix"],
"signalTurnsBefore": 3
}
```
### Project config
`.cursor/.supermemory/config.json` holds per-workspace overrides and wins over global config. Add it to `.gitignore` if it contains an API key.
```json
{
"apiKey": "sm_...",
"repoContainerTag": "repo_my_project__0123456789abcdef",
"similarityThreshold": 0.55,
"maxMemories": 10,
"injectProfile": true
}
```
| Option | Default | Description |
|--------|---------|-------------|
| `apiKey` | — | Project-specific API key |
| `baseUrl` | Supermemory API | Override the Supermemory API base URL |
| `repoContainerTag` | derived from normalized Git remote or project path | Override the unified repository container |
| `userContainerTag` | — | Legacy Cursor personal container to continue reading |
| `projectContainerTag` | — | Legacy Cursor project container to continue reading |
| `similarityThreshold` | `0.55` | Minimum similarity for prompt recall. Values below `0.55` are floored. |
| `maxMemories` | `10` | Max profile facts injected at session start |
| `injectProfile` | `true` | Whether to inject the user profile at session start |
| `signalExtraction` | `false` | Capture only turns containing durable-signal keywords |
| `signalKeywords` | `remember`, `architecture`, `decision`, `bug`, `fix` | Keywords that trigger signal-based capture |
| `signalTurnsBefore` | `3` | Number of nearby turns retained around a signal |
You can also set these from the agent with `supermemory_set_config`, or edit the file by hand.
## Log Out
Run `/supermemory-logout` in Cursor.
This removes the stored credentials. Your memories in Supermemory are preserved.
## Next Steps
<Card title="GitHub Repository" icon="/images/github-icon.svg" href="https://github.com/supermemoryai/cursor-supermemory">
Source code, issues, and detailed README.
</Card>

View file

@ -0,0 +1,64 @@
---
title: "Grok Bot"
sidebarTitle: "Grok Bot"
description: "Supermemory for Grok Bot: persistent memory for your Grok Bots"
icon: "/images/grok-logo.png"
---
Grok Bots are cloud agents. A new task can mean a new machine and a blank context. Supermemory is the memory they keep between those tasks — findings, decisions, and preferences — so the next Bot does not start cold.
## How it helps
Without memory, a Bot redoes investigation you already paid for. With Supermemory:
- Later Bots know what earlier ones already figured out
- You can save a decision or finding once and have it stick
- You can ask what it already remembers instead of starting over
Install once, sign in, and it is available to your Grok Bots.
## Install
Open the [Supermemory for Grok Bot plugin page](https://x.ai/bot/plugin/58578698) and select **Add to Grok Bot**.
Restart Grok Bot afterwards so the plugin loads.
## Authenticate
Just ask Grok Bot:
```
Sign me in to Supermemory
```
It shows a connect card. Follow it to link your Supermemory account.
To check later, ask `Is Supermemory connected?`
## Skills
Grok Bot picks these up from what you ask. There are no slash commands.
| Skill | Ask for it like this |
|-------|----------------------|
| `memory-init` | "Learn this codebase and remember it" |
| `memory-save` | "Remember that we use Vitest for unit tests" |
| `memory-search` | "What do you remember about our database schema?" |
## Log Out
Ask Grok Bot to `Disconnect Supermemory`.
This removes the stored credentials. Your memories in Supermemory are preserved.
## Next Steps
<CardGroup cols={2}>
<Card title="GitHub Repository" icon="/images/github-icon.svg" href="https://github.com/supermemoryai/cursor-supermemory">
Source code, issues, and detailed README.
</Card>
<Card title="Cursor Plugin" icon="/images/cursor-logo.svg" href="/integrations/cursor">
The same plugin, installed through Cursor.
</Card>
</CardGroup>

View file

@ -18,7 +18,7 @@ Supermemory integrates with [VoltAgent](https://github.com/VoltAgent/voltagent),
## Installation
```bash
npm install @supermemory/tools @voltagent/core
npm install @supermemory/tools @voltagent/core ai@^6 @ai-sdk/openai@^3
```
Set up your API key as an environment variable:
@ -52,9 +52,7 @@ const configWithMemory = withSupermemory({
const agent = new Agent(configWithMemory)
// Memories are automatically injected and saved
const result = await agent.generateText({
messages: [{ role: "user", content: "What's my name?" }],
})
const result = await agent.generateText("What's my name?")
```
<Note>
@ -131,14 +129,13 @@ const configWithMemory = withSupermemory({
// Search tuning
searchMode: "hybrid", // "memories" | "documents" | "hybrid"
threshold: 0.1, // 0.0-1.0 (higher = more accurate)
limit: 10, // Max results to return
threshold: 0.6, // 0.0-1.0 (higher = more accurate)
limit: 10, // Integer from 1 to 100
rerank: true, // Rerank for best relevance
rewriteQuery: false, // AI-rewrite query (+400ms latency)
// Context
entityContext: "This is John, a software engineer", // Guides memory extraction (max 1500 chars)
metadata: { source: "voltagent" }, // Attached to saved conversations
metadata: { source: "voltagent" }, // Attached to saved conversations
// API
apiKey: "sk-...", // Falls back to SUPERMEMORY_API_KEY env var
@ -154,14 +151,16 @@ const configWithMemory = withSupermemory({
| `addMemory` | string | `"always"` | Whether to save conversations after each response |
| `customId` | string | **required** | Custom ID to group messages into a conversation |
| `searchMode` | string | — | `"memories"`, `"documents"`, or `"hybrid"` |
| `threshold` | number | `0.1` | Similarity threshold (0 = more results, 1 = more accurate) |
| `limit` | number | `10` | Maximum number of memory results |
| `threshold` | number | — | Similarity threshold (0 = more results, 1 = more accurate) |
| `limit` | number | — | Maximum number of memory results (integer from 1 to 100) |
| `rerank` | boolean | `false` | Rerank results for relevance |
| `rewriteQuery` | boolean | `false` | AI-rewrite query for better results (+400ms) |
| `entityContext` | string | — | Context for memory extraction (max 1500 chars) |
| `entityContext` | string | — | Deprecated and ignored. [Configure it on the container tag instead](/concepts/customization#entity-context). |
| `metadata` | object | — | Custom metadata attached to saved conversations |
| `promptTemplate` | function | — | Custom function to format memory data into prompt |
When `threshold` or `limit` is omitted, the selected Supermemory backend route applies its own default. Set them explicitly when you need consistent search tuning across modes.
## Search Modes
The `searchMode` option controls what type of results are searched:
@ -171,4 +170,3 @@ The `searchMode` option controls what type of results are searched:
| `"memories"` | Search only memory entries (atomic facts about the user) |
| `"documents"` | Search only document chunks |
| `"hybrid"` | Search both memories AND document chunks (recommended) |

View file

@ -36,7 +36,7 @@ A malicious or buggy client with a correctly scoped key cannot read another cont
### Data use
Supermemory is infrastructure for *your* agents. Paid production usage is not treated as free training corpus for unrelated public models. For contractual wording (DPA, subprocessors, training policies), request the latest legal pack from support.
Supermemory is infrastructure for *your* agents. Your customer content is never used to train models — this applies to every plan, free or paid, with no difference between them. For contractual wording (DPA, subprocessors, training policies), request the latest legal pack from support.
### Data residency and deployment options

View file

@ -6,17 +6,13 @@ icon: "book-open"
mode: "wide"
---
export const BuildingBlock = ({ icon, title, href }) => {
export const BuildingBlock = ({ title, href, children }) => {
return (
<a
href={href}
className="group flex flex-row items-center gap-4 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900/40 p-4 no-underline transition-all duration-200 hover:border-gray-300 dark:hover:border-zinc-600 hover:shadow-sm"
>
<img
src={icon}
alt=""
className="h-16 w-16 shrink-0 object-contain"
/>
{children}
<span className="text-sm font-medium leading-snug text-gray-900 dark:text-zinc-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
{title}
</span>
@ -30,50 +26,69 @@ It provides all the building blocks — Memory, Retrieval, Profiles, Connectors,
<div className="not-prose my-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<BuildingBlock
icon="/images/building-blocks/memory-router.svg"
title="Memory & Continual Learning"
href="/concepts/graph-memory"
/>
>
<img src="/images/building-blocks/memory-router.svg" alt="" className="h-16 w-16 shrink-0 object-contain" />
</BuildingBlock>
<BuildingBlock
icon="/images/building-blocks/document-retrieval.svg"
title="SuperRAG (Retrieval)"
href="/concepts/super-rag"
/>
>
<img src="/images/building-blocks/document-retrieval.svg" alt="" className="h-16 w-16 shrink-0 object-contain" />
</BuildingBlock>
<BuildingBlock
icon="/images/building-blocks/file-systems.svg"
title="Filesystems"
href="/smfs/overview"
/>
>
<img src="/images/building-blocks/file-systems.svg" alt="" className="h-16 w-16 shrink-0 object-contain" />
</BuildingBlock>
<BuildingBlock
icon="/images/building-blocks/user-profiles.svg"
title="Profiles"
href="/concepts/user-profiles"
/>
>
<img src="/images/building-blocks/user-profiles.svg" alt="" className="h-16 w-16 shrink-0 object-contain" />
</BuildingBlock>
<BuildingBlock
icon="/images/building-blocks/connectors.svg"
title="Connectors"
href="/connectors/overview"
/>
>
<img src="/images/building-blocks/connectors.svg" alt="" className="h-16 w-16 shrink-0 object-contain" />
</BuildingBlock>
<BuildingBlock
icon="/images/building-blocks/extractor.svg"
title="Extractors"
href="/concepts/content-types"
/>
>
<img src="/images/building-blocks/extractor.svg" alt="" className="h-16 w-16 shrink-0 object-contain" />
</BuildingBlock>
<BuildingBlock
icon="/images/building-blocks/qualitative-analysis.svg"
title="Qualitative Analysis"
href="https://supermemory.ai/research"
/>
>
<img src="/images/building-blocks/qualitative-analysis.svg" alt="" className="h-16 w-16 shrink-0 object-contain" />
</BuildingBlock>
<BuildingBlock
icon="/images/building-blocks/hermes.svg"
title="Plugins"
href="/supermemory-mcp/mcp"
/>
>
<img
src="/images/building-blocks/hermes.svg"
alt=""
className="h-16 w-16 shrink-0 object-contain"
style={{ transform: "scale(0.85)" }}
/>
</BuildingBlock>
<BuildingBlock
icon="/images/building-blocks/brain-head.png"
title="Company Brain"
href="/company-brain/overview"
/>
>
<img
src="/images/building-blocks/brain-head.png"
alt=""
className="h-16 w-16 shrink-0 object-contain"
style={{ transform: "scale(1.3)" }}
/>
</BuildingBlock>
</div>
With supermemory, developers can provide perfect recall about their users to build AI agents that are more intelligent, more personalized, and more consistent.

View file

@ -173,7 +173,7 @@ The memory will no longer appear in search results but remains in the database (
## Forget Matching
Forget **everything** about a topic in one call. You give a prompt or a query; the service semantically searches the container's memories, an LLM decides which ones are genuinely about your target, and those are soft-deleted. Use this for "forget everything about X" rather than deleting memories one by one.
Forget in bulk in one call, two ways. Give a **`query`** (a prompt or topic) and the service semantically searches the container, an LLM decides which memories are genuinely about your target, and those are soft-deleted — use this for "forget everything about X". Or give an explicit **`ids`** list to forget exactly those memories with no search. Provide one or the other.
<Warning>
This is a bulk, destructive operation. Always **`dryRun` first** to review what would be forgotten, then re-run with `dryRun: false`. The match is semantic, so a too-broad query can select more than you intend — `threshold` and `maxForget` bound the blast radius.
@ -197,7 +197,7 @@ This is a bulk, destructive operation. Always **`dryRun` first** to review what
}).then((r) => r.json());
// preview.candidates → [{ id, memory, score }, ...]
// 2) Apply
// 2) Apply — pass the ids from the preview to forget exactly that set
const result = await fetch("https://api.supermemory.ai/v4/memories/forget-matching", {
method: "POST",
headers: {
@ -205,7 +205,7 @@ This is a bulk, destructive operation. Always **`dryRun` first** to review what
"Content-Type": "application/json"
},
body: JSON.stringify({
query: "forget everything about Project Titan",
ids: preview.candidates.map((c) => c.id),
containerTag: "user_123",
dryRun: false,
reason: "project cancelled"
@ -227,12 +227,12 @@ This is a bulk, destructive operation. Always **`dryRun` first** to review what
"dryRun": true
}'
# Apply
# Apply — pass the ids from the preview to forget exactly that set
curl -X POST "https://api.supermemory.ai/v4/memories/forget-matching" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "forget everything about Project Titan",
"ids": ["abc123", "def456", "ghi789"],
"containerTag": "user_123",
"dryRun": false,
"reason": "project cancelled"
@ -245,13 +245,16 @@ This is a bulk, destructive operation. Always **`dryRun` first** to review what
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | yes | What to forget — a natural-language instruction ("forget everything about Project Titan") or a bare topic ("Project Titan") |
| `query` | string | one of* | What to forget — a natural-language instruction ("forget everything about Project Titan") or a bare topic ("Project Titan") |
| `ids` | string[] | one of* | Exact memory ids to forget instead of a `query` — no semantic search. Ids are validated against `containerTag`, so unknown or out-of-scope ids are ignored |
| `containerTag` | string | yes | Container tag / space to scope the operation to |
| `dryRun` | boolean | no | When `true`, returns what *would* be forgotten without changing anything. Defaults to `false` |
| `threshold` | number | no | Similarity floor (0–1) for candidate memories. Lower casts a wider net. Defaults to `0.5` |
| `maxForget` | number | no | Safety cap on how many memories may be forgotten in one call (1–500). Defaults to `100` |
| `threshold` | number | no | Similarity floor (0–1) for candidate memories (`query` mode only). Lower casts a wider net. Defaults to `0.5` |
| `maxForget` | number | no | Safety cap for **query mode** — the most matches one call may forget (1–500). Defaults to `100`. Ignored in id mode, which forgets exactly the ids you pass (bounded only by the 500-item array limit) |
| `reason` | string | no | Reason recorded as `forgetReason` on each forgotten memory |
\* Provide either `query` or `ids`.
### Response
```json
@ -279,6 +282,10 @@ This is a bulk, destructive operation. Always **`dryRun` first** to review what
Identity is server-owned: the LLM only ever references opaque handles for the memories a search returned, so it can never forget a memory outside the results it reviewed, and every operation is scoped to the `containerTag` you pass.
</Note>
<Tip>
**Exact, bound deletes.** Applying with a `query` re-runs the semantic match, so the result can drift from the preview if the container changed in between. To forget *precisely* what you reviewed, take the `id`s from a `dryRun` preview and send them back as `ids` on the apply — the delete is then bound to exactly that set. (`ids` with `dryRun: true` returns the validated set as `candidates` without deleting, so you can confirm first.)
</Tip>
---
## Update Memory (Versioned)

View file

@ -129,6 +129,14 @@ Use the dimension published for your chosen model. A mismatch with vectors alrea
**Changing embeddings later:** Not supported in place. Start from a fresh data directory or re-ingest all content so vectors stay comparable.
> [!IMPORTANT]
> **Model Mixing Bug in v0.0.5 (Exact match returns nothing)**
>
> In version `v0.0.5`, there was a bug where the server could mix different embedding models between write and read paths (e.g., document ingestion using OpenAI but memory queries using local default embeddings). In multilingual contexts like Japanese (which lacks space tokenization for fallback lexical FTS matching), this caused exact-text memory searches through `/v4/search` and `/v4/profile` to silently return `{"results":[],"total":0}`.
>
> **Resolution:**
> This was fully resolved in `v0.0.7` by locking the embedding plan uniformly across all document and query embedding paths (enforced via a locked plan in the database store). If you are running `v0.0.5` and experiencing this issue, you should upgrade to `v0.0.7` or later.
## Related
- [Configuration](/self-hosting/configuration) — LLM providers, storage, ingestion limits

View file

@ -27,6 +27,28 @@ bunx supermemory local
The installer detects your OS and architecture, downloads the right binary, verifies it, and (when run interactively) prompts you for an LLM API key. Supported platforms: macOS (Apple Silicon & Intel), Linux (x64 & arm64).
### Pin or change versions
Pass an explicit version to install (or roll back to) a specific release instead of `latest`:
```bash
curl -fsSL https://supermemory.ai/install | bash -s -- 0.0.3
```
<Warning>
Before rolling back, back up your [data directory](#where-things-live). The installer replaces the binary, but an older server may not understand data or schema changes made by a newer release.
</Warning>
Release tags are `server-v<version>` on [GitHub Releases](https://github.com/supermemoryai/supermemory/releases) (for example [`server-v0.0.3`](https://github.com/supermemoryai/supermemory/releases/tag/server-v0.0.3)).
To move to the newest release later:
```bash
supermemory-server upgrade
```
The binary may also print an “update available” notification on startup. If you intentionally pinned an older version (for example while debugging a regression), you can ignore that message until you are ready to upgrade.
## Run
```bash

View file

@ -1,10 +1,3 @@
export const DHRAVYA = {
name: "Dhravya Shah",
avatar: "/images/company-brain/dhravya-slack-icon.jpg",
};
export const BOT_AVATAR = "/images/company-brain/supermemory-slack-icon.png";
export const Mention = ({ children, self = false }) => (
<span
className="rounded px-1 py-px font-medium"
@ -53,9 +46,13 @@ export const SlackThread = ({
private: isPrivate = false,
members,
dmWith,
hasAvatar = false,
maxHeight = "70vh",
children,
}) => {
const childItems = Array.isArray(children) ? children : [children];
const dmAvatar = hasAvatar ? childItems[0] : null;
const threadMessages = hasAvatar ? childItems.slice(1) : children;
const composerLabel =
type === "dm"
? `Message ${dmWith?.name || "someone"}`
@ -91,11 +88,9 @@ export const SlackThread = ({
) : type === "dm" ? (
<div className="flex items-center gap-2 border-b border-black/10 px-4 py-3 dark:border-white/10">
<div className="relative h-6 w-6 flex-none">
<img
src={dmWith?.avatar || "/images/company-brain/supermemory-slack-icon.png"}
alt={dmWith?.name || "DM"}
className="h-6 w-6 rounded-md object-cover"
/>
<span className="block h-full w-full overflow-hidden rounded-md [&_img]:h-full [&_img]:w-full [&_img]:object-cover">
{dmAvatar}
</span>
<span className="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border-2 border-white bg-[#2BAC76] dark:border-[#1A1D21]" />
</div>
<span className="truncate text-[15px] font-bold text-gray-900 dark:text-zinc-50">
@ -149,7 +144,7 @@ export const SlackThread = ({
className="overflow-y-auto bg-white dark:bg-[#1A1D21]"
style={maxHeight ? { maxHeight } : undefined}
>
{children}
{threadMessages}
</div>
{maxHeight ? (
<div
@ -213,7 +208,7 @@ export const SlackMessage = ({
bot = false,
time,
color = "#611f69",
avatar,
hasAvatar = false,
badges,
reactions,
highlighted = false,
@ -221,11 +216,11 @@ export const SlackMessage = ({
children,
}) => {
const SELF_NAME = "Dhravya Shah";
const SELF_AVATAR = "/images/company-brain/dhravya-slack-icon.jpg";
const BOT_AVATAR = "/images/company-brain/supermemory-slack-icon.png";
const childItems = Array.isArray(children) ? children : [children];
const resolvedAvatar = hasAvatar ? childItems[0] : null;
const messageBody = hasAvatar ? childItems.slice(1) : children;
const resolvedName = bot ? "supermemory" : self ? SELF_NAME : name || SELF_NAME;
const resolvedAvatar = bot ? BOT_AVATAR : self ? SELF_AVATAR : avatar;
const resolvedBadges = badges || (bot ? ["AGENT"] : null);
return (
@ -248,11 +243,9 @@ export const SlackMessage = ({
))}
</div>
{resolvedAvatar ? (
<img
src={resolvedAvatar}
alt={resolvedName}
className="mt-0.5 h-9 w-9 flex-none rounded-md object-cover"
/>
<span className="mt-0.5 h-9 w-9 flex-none overflow-hidden rounded-md [&_img]:h-full [&_img]:w-full [&_img]:object-cover">
{resolvedAvatar}
</span>
) : (
<div
className="mt-0.5 flex h-9 w-9 flex-none items-center justify-center rounded-md text-sm font-bold text-white"
@ -286,7 +279,7 @@ export const SlackMessage = ({
) : null}
</div>
<div className="text-[15px] leading-normal text-gray-800 dark:text-zinc-200 [&_p]:m-0 [&_strong]:font-bold [&_strong]:text-gray-900 dark:[&_strong]:text-zinc-50 [&_code]:rounded [&_code]:bg-black/5 [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-[0.9em] dark:[&_code]:bg-white/10">
{children}
{messageBody}
</div>
{reactions ? (
<div className="mt-1.5 flex flex-wrap gap-1">

View file

@ -0,0 +1,245 @@
---
title: "ChatGPT Web"
description: "Connect Supermemory MCP to ChatGPT Web and make Supermemory the default for memory, recall, and saved context"
icon: "/images/openai.svg"
sidebarTitle: "ChatGPT Web"
---
Connect Supermemory MCP to [ChatGPT Web](https://chatgpt.com) to use your Supermemory context directly in a conversation.
## Step 1: Open Settings
Open your profile menu in the lower-left corner of ChatGPT Web and select **Settings**.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-1-settings-dark.png"
alt="ChatGPT Web profile menu in dark mode with Settings highlighted"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-1-settings-light.png"
alt="ChatGPT Web profile menu in light mode with Settings highlighted"
/>
## Step 2: Open Plugins
Select **Plugins** in the Settings sidebar.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-2-plugins-dark.png"
alt="ChatGPT Web Plugins settings in dark mode"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-2-plugins-light.png"
alt="ChatGPT Web Plugins settings in light mode"
/>
## Step 3: Open Developer mode
Scroll to the bottom of **Plugins** and select **Developer mode**.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-3-plugins-bottom-dark.png"
alt="Developer mode and Browse plugins at the bottom of ChatGPT Web Plugins settings in dark mode"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-3-plugins-bottom-light.png"
alt="Developer mode and Browse plugins at the bottom of ChatGPT Web Plugins settings in light mode"
/>
## Step 4: Enable Developer mode
ChatGPT opens the **Developer mode** section under **Security and login**. Turn **Developer mode** on.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-4-developer-mode-page-dark.png"
alt="Developer mode section under Security and login in ChatGPT Web dark mode"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-4-developer-mode-page-light.png"
alt="Developer mode section under Security and login in ChatGPT Web light mode"
/>
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-4-developer-mode-on-dark.png"
alt="Developer mode enabled in ChatGPT Web dark mode"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-4-developer-mode-on-light.png"
alt="Developer mode enabled in ChatGPT Web light mode"
/>
## Step 5: Open Browse plugins
Return to **Plugins**, scroll to the bottom, and select **Browse plugins**.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-3-plugins-bottom-dark.png"
alt="Browse plugins at the bottom of ChatGPT Web Plugins settings in dark mode"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-3-plugins-bottom-light.png"
alt="Browse plugins at the bottom of ChatGPT Web Plugins settings in light mode"
/>
## Step 6: Add a plugin
On the Plugins page, select the **plus icon** in the upper-right corner.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-6-plugin-directory-dark.png"
alt="ChatGPT Web Plugins page in dark mode with the plus icon"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-6-plugin-directory-light.png"
alt="ChatGPT Web Plugins page in light mode with the plus icon"
/>
## Step 7: Enter the Supermemory details
Complete the **New Plugin** form:
- In **Name**, enter `Supermemory MCP`.
- In **Description**, enter `Memory/context for your AI agents`.
- Under **Connection**, select **Server URL** instead of **Tunnel**.
- In the URL field, paste `https://mcp.supermemory.ai/mcp`.
The icon is optional. Leave **Advanced OAuth settings** unchanged.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-7-details-dark.png"
alt="New Plugin form in dark mode filled with the Supermemory MCP details"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-7-details-light.png"
alt="New Plugin form in light mode filled with the Supermemory MCP details"
/>
## Step 8: Confirm and create
Scroll to the bottom of the form. Select **I understand and want to continue**, then select **Create**.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-8-confirm-dark.png"
alt="New Plugin confirmation in dark mode with the acknowledgement selected"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-8-confirm-light.png"
alt="New Plugin confirmation in light mode with the acknowledgement selected"
/>
## Step 9: Sign in to Supermemory
ChatGPT opens the **Add Supermemory MCP to ChatGPT** dialog. Select **Sign in with Supermemory MCP**.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-9-sign-in-dark.png"
alt="ChatGPT authorization dialog for Supermemory MCP in dark mode"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-9-sign-in-light.png"
alt="ChatGPT authorization dialog for Supermemory MCP in light mode"
/>
<Accordion title="If Supermemory asks you to choose an organization">
Accounts with access to more than one organization may see an organization picker. Select the organization you want ChatGPT Web to use.
![Supermemory organization picker shown for accounts with multiple organizations](/images/supermemory-mcp/chatgpt-web/step-9-organization.png)
</Accordion>
## Step 10: Approve access
On the Supermemory authorization page:
- Select **Read + Write** to let ChatGPT search and save Supermemory context. Select **Read only** if you only want search access.
- Select **Full access** to use every available space, or **Scoped** to choose specific spaces.
- Choose how long the connection should remain active.
- Select **Approve**.
![Supermemory MCP authorization page with permission, access, and expiration controls](/images/supermemory-mcp/chatgpt-web/step-10-authorize.png)
## Step 11: Start using Supermemory
After approval, ChatGPT returns to the Supermemory MCP plugin page. Select **Try in chat**.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/step-11-connected-dark.png"
alt="Connected Supermemory MCP page in ChatGPT Web dark mode"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/step-11-connected-light.png"
alt="Connected Supermemory MCP page in ChatGPT Web light mode"
/>
## Done
Supermemory MCP is now ready to use in ChatGPT Web. You can manage or disconnect it later from the [Plugins page](https://chatgpt.com/plugins).
## Frequently asked questions
<AccordionGroup>
<Accordion title="How do I make Supermemory the default memory in ChatGPT?">
<Steps>
<Step title="Turn off ChatGPT memory">
Turn **Enable memory** off.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/faq-memory-dark.png"
alt="ChatGPT Personalization settings in dark mode showing the Enable memory switch and Memory summary"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/faq-memory-light.png"
alt="ChatGPT Personalization settings in light mode showing the Enable memory switch and Memory summary"
/>
Turning memory off does not delete your past chats.
</Step>
<Step title="Optionally copy your existing memory">
If you want to keep a copy of ChatGPT's existing memory, select **Manage** and copy the memory summary. Open a chat with Supermemory enabled, paste the summary, and say `Add this to Supermemory`.
</Step>
<Step title="Make Supermemory the default">
Paste this into **Custom instructions**:
<img
className="hidden dark:block"
src="/images/supermemory-mcp/chatgpt-web/faq-custom-instructions-dark.png"
alt="ChatGPT Personalization settings in dark mode showing the Custom instructions field"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/chatgpt-web/faq-custom-instructions-light.png"
alt="ChatGPT Personalization settings in light mode showing the Custom instructions field"
/>
```text
Use Supermemory MCP as my default memory and workspace memory whenever its tools are available. Use it first for both recall and saving new memories.
```
</Step>
</Steps>
</Accordion>
</AccordionGroup>

View file

@ -5,7 +5,7 @@ icon: "monitor"
sidebarTitle: "Claude Desktop"
---
This guide walks through adding Supermemory as a **custom connector** in [Claude](https://claude.ai) (Desktop or web): open **Settings → Connectors**, add the MCP URL, connect, and authorize. For other clients and API-key auth, see [Setup and Usage](/supermemory-mcp/setup).
This guide walks through adding Supermemory as a **custom connector** in [Claude](https://claude.ai) (Desktop or web): open **Settings → Connectors**, add the MCP URL, connect, and authorize. For other clients, see [Setup and Usage](/supermemory-mcp/setup).
## Step 1 — Open Connectors and add a custom connector

View file

@ -1,55 +1,27 @@
---
title: "Overview"
description: "Unified memory for Claude, Cursor, and every MCP client — one layer across all your tools"
description: "Give every MCP-compatible assistant shared memory, team spaces, and interactive workflows"
icon: "brain-circuit"
---
Most AI tools forget you the moment the tab closes. You re-explain preferences, restate project context, and re-teach the same lessons in Claude, Cursor, ChatGPT, and everything else.
Supermemory MCP gives every MCP-compatible assistant a shared memory layer, so technical and non-technical teams can collaborate with AI using the same authorized context. Connect once to search decisions, save knowledge, upload source material, and explore relationships from the AI tools your team already uses.
**Supermemory MCP** is a single memory layer that plugs into any MCP-compatible client. Connect once, and the same long-term memory follows you across tools — coding agents, chat apps, IDEs, and whatever you add next.
## What you get
- **Unified memory across tools** — Facts you save in Claude are available in Cursor (and vice versa). One brain, many surfaces.
- **Memory that compounds** — Preferences, decisions, and project knowledge accumulate instead of resetting every session.
- **Profiles that stay current** — Supermemory builds a living user profile from what you share, so assistants start with who you are — not a blank slate.
- **Project-scoped context** — Keep work, personal, and client work separate with optional project tags.
- **Works where you already work** — Claude (Connectors), Cursor, Windsurf, VS Code, Cline, and any client that speaks MCP.
### Why a small tool surface is intentional
Supermemory MCP exposes a **minimal** set of tools on purpose.
Assistants don’t need a kitchen-sink API to remember well. They need a few durable actions: save what matters, recall what’s relevant, and know who the user is. Fewer tools means less confusion for the model, clearer behavior, and more reliable use in production.
| Surface | Role |
| --- | --- |
| **`memory`** | Save or forget something durable |
| **`recall`** | Search memories + optionally load the user profile |
| **`whoAmI`** | Confirm the authenticated user / session |
| **`context` prompt** | Inject a ready-to-use profile system message (`/context` in many clients) |
| **Profile / projects resources** | Raw profile and project list for clients that read MCP resources |
That’s enough for agents to build real continuity — without tool sprawl.
## How it fits together
1. You connect your client to `https://mcp.supermemory.ai/mcp` (OAuth).
2. During conversations, the model stores important facts with **`memory`**.
3. When context is needed, **`recall`** (and the profile) pull the right history back in.
4. Switch tools tomorrow — same account, same memory.
Under the hood, the server runs on **Cloudflare Workers** with Durable Objects for scalable, sticky sessions. Your data is isolated per account; open-source implementation is on GitHub.
Engineering and research teams can carry project context across assistants, while finance, legal, medical, and operations teams can work from their own source material. Ask naturally. The assistant selects the right Supermemory tool without requiring code or tool names. See the [tools reference](#tools) for exact inputs and results.
## Connect
Server URL:
Add this remote MCP server to any compatible client:
```text
https://mcp.supermemory.ai/mcp
```
Add it to your MCP client config:
After you connect, your client opens Supermemory in a browser. Sign in or create an account, then choose which spaces the client can access. Supermemory uses OAuth, so no API key is required.
For client-specific instructions, see [Setup and Usage](/supermemory-mcp/setup) or the [Claude Desktop guide](/supermemory-mcp/claude-desktop).
<Accordion title="Manual JSON configuration" icon="code">
MCP clients that use JSON configuration generally accept this shape:
```json
{
@ -60,92 +32,142 @@ Add it to your MCP client config:
}
}
```
</Accordion>
The server requires **OAuth**. Your client will discover the authorization server via `/.well-known/oauth-protected-resource` and prompt you to authenticate.
## How spaces work
For Claude (Settings → Connectors), see **[Claude Desktop](/supermemory-mcp/claude-desktop)**. For client-specific examples, see **[Setup and Usage](/supermemory-mcp/setup)**.
A **space** keeps a team's documents, memories, and profile context focused, so AI retrieves the right knowledge without mixing unrelated work. Use separate spaces for an engineering launch, research project, finance workflow, legal matter, medical knowledge base, or any other shared initiative. Teammates collaborate within the spaces they are allowed to read or write.
### Project Scoping
Scope all operations to a specific project with `x-sm-project`:
```json
{
"mcpServers": {
"supermemory": {
"url": "https://mcp.supermemory.ai/mcp",
"headers": {
"x-sm-project": "your-project-id"
}
}
}
}
```
Name a space to use it for one request without changing your active space. Otherwise, Supermemory uses your active space or account default. Ask to switch spaces when you want future requests to use a different active space.
## Tools
### `memory`
Your assistant chooses these tools automatically. Use this table when you need the exact inputs and results.
Save or forget information about the user.
| Tool | Use it for | Inputs | Result |
| --- | --- | --- | --- |
| `search_memory` | Semantic recall from one space, with optional profile context | `query` (required), `includeProfile`, `containerTag` | Profile context and matching memories |
| `add_memory` | Save information or forget outdated information | `content` (required), `action` (`save` or `forget`), `containerTag` | Save or forget confirmation |
| `listDocuments` | Browse stored source documents and their summaries | `page`, `limit`, `containerTag` | Document IDs, titles, types, status, dates, and summaries |
| `getDocument` | Read the available content of one document | `documentId` (required) | Document metadata, summary, and available content |
| `listMemories` | Browse recent extracted memory entries and their source document IDs | `page`, `limit`, `containerTag` | Memory IDs, text, versions, and source document IDs |
| `listSpaces` | List accessible spaces and resolve a space name to its key | None | Formatted list plus structured `spaces` and `count` fields |
| `whoAmI` | Inspect the authenticated account, permissions, scope, and active space | None | Account and access context |
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `content` | string | Yes | The memory content to save or forget |
| `action` | `"save"` \| `"forget"` | No | Default: `"save"` |
| `containerTag` | string | No | Project tag to scope the memory |
### Search
### `recall`
`search_memory` accepts a natural-language query and returns semantically relevant memories. By default, it also includes stable and recent profile context from the same space. Set `includeProfile` to `false` when only matching memories are needed.
Search memories and get user profile.
Use the retrieval tools for different questions:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | Search query to find relevant memories |
| `includeProfile` | boolean | No | Include user profile summary. Default: `true` |
| `containerTag` | string | No | Project tag to scope the search |
- Use `search_memory` to answer a question from remembered context.
- Use `listDocuments` to discover stored sources, then `getDocument` to read one source in full.
- Use `listMemories` to inspect the extracted memory entries themselves, including their IDs and source document IDs.
### `whoAmI`
`listDocuments` and `listMemories` default to 10 results per page and accept up to 50.
Get the current logged-in user's information. Returns `{ userId, email, name, client, sessionId }`.
### Save or forget
## Resources
`add_memory` saves the supplied `content` by default. Set `action` to `forget` when a fact is outdated or should be removed. There is no separate forget tool.
| URI | Description |
|-----|-------------|
| `supermemory://profile` | User profile with stable preferences and recent activity |
| `supermemory://projects` | List of available memory projects |
If the content is already final, the assistant should use `add_memory`. If you want to review, edit, or choose a space before saving, it should open the `guided-save` widget instead.
## Prompts
### Access control
### `context`
`whoAmI` returns the current identity, role, access type, granted scope, and active space. `listSpaces` returns only spaces the authenticated account can access, with names, keys, document and memory counts, and recent activity.
Inject user profile and preferences as system context for AI conversations. Returns a formatted message with the user's stable preferences and recent activity.
## Interactive widgets
In Cursor and Claude Code you can often invoke this with **`/context`**, which gives the model enough profile context to use and query Supermemory effectively.
Clients that support [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) can open these Supermemory widgets directly inside the conversation.
**Purpose:** Unlike the `recall` tool (search for specific information) or the `profile` resource (raw data), the `context` prompt is a pre-formatted system message for conversation start.
| Tool | What it opens |
| --- | --- |
| `select-space` | A searchable space picker |
| `guided-save` | An editable memory form with a space selector |
| `upload-file` | A local file picker and upload form |
| `memory-graph` | An interactive graph of documents and memories |
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `containerTag` | string | No | Project tag to scope the profile (max 128 chars) |
| `includeRecent` | boolean | No | Include recent activity in the profile. Default: `true` |
### Select a space
**Output format:**
- Instructions to save new memories using the `memory` tool
- **Stable Preferences:** Long-term user facts and preferences
- **Recent Activity:** Recent interactions and context (when `includeRecent` is `true`)
- Fallback message when no profile exists yet
`select-space` opens a searchable space picker and changes the active space for future Supermemory actions. A one-off request in another space does not change the active space.
**When to use:**
- **`context` prompt** — automatic system context at conversation start
- **`recall` tool** — search for specific information
- **`profile` resource** — raw profile data for custom processing
<img
className="hidden dark:block"
src="/images/supermemory-mcp/widgets/select-space.png"
alt="Supermemory MCP searchable space picker"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/widgets/select-space-dark.png"
alt="Supermemory MCP searchable space picker"
/>
### Guided save
`guided-save` opens a draft with editable memory content and a writable-space selector. The assistant can prefill the draft, but nothing is saved until you submit it.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/widgets/guided-save-draft.png"
alt="Supermemory MCP guided save draft with editable memory content and a space selector"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/widgets/guided-save-dark.png"
alt="Supermemory MCP guided save draft with editable memory content and a space selector"
/>
### File upload
`upload-file` opens a local file picker and uploads one file at a time to a writable space.
Supported types include text, Markdown, PDF, Word, CSV, common images, MP3, WAV, M4A, MP4, and WebM.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/widgets/upload-file.png"
alt="Supermemory MCP file picker with a writable-space selector"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/widgets/upload-file-dark.png"
alt="Supermemory MCP file picker with a writable-space selector"
/>
### Memory graph
`memory-graph` renders the selected space as an interactive graph of source documents and extracted memories.
When no space is named, the server automatically uses the active space or account default. If you name a specific space, the assistant calls `listSpaces` to resolve its name to a space key, then opens that space's graph.
<img
className="hidden dark:block"
src="/images/supermemory-mcp/widgets/memory-graph.png"
alt="Supermemory MCP interactive graph of documents and memories"
/>
<img
className="block dark:hidden"
src="/images/supermemory-mcp/widgets/memory-graph-dark.png"
alt="Supermemory MCP interactive graph of documents and memories"
/>
## Resources and context prompt
Some MCP clients also expose resources and prompts:
| Kind | Name or URI | What it returns |
| --- | --- | --- |
| Resource | `supermemory://profile` | Stable and recent profile context for the active space |
| Resource | `supermemory://spaces` | A compact list of accessible spaces with the active space marked |
| Prompt | `context` | A ready-to-attach context message for the active space, plus up to three recently active spaces |
The `context` prompt takes no arguments. It returns profile context for the active space and up to three recently active spaces.
<CardGroup cols={2}>
<Card title="Setup and Usage" icon="settings" href="/supermemory-mcp/setup">
Client configs, OAuth, and project scoping.
Configure Supermemory in supported MCP clients.
</Card>
<Card title="MCP Server Source" icon="/images/github-icon.svg" href="https://github.com/supermemoryai/supermemory/tree/main/apps/mcp">
Open-source implementation.
<Card title="Supermemory MCP on GitHub" icon="/images/github-icon.svg" href="https://github.com/supermemoryai/supermemory/tree/main/apps/mcp">
View the source code.
</Card>
</CardGroup>

View file

@ -1,7 +1,7 @@
---
title: 'Setup and Usage'
description: 'How to set up and use Supermemory MCP Server 4.0'
icon: 'settings'
title: "Setup and Usage"
description: "Connect Supermemory to an MCP client with OAuth and choose how requests use spaces"
icon: "settings"
---
## Server URL
@ -10,7 +10,7 @@ icon: 'settings'
https://mcp.supermemory.ai/mcp
```
Add this to your MCP client config (Claude, Cursor, Windsurf, VS Code, etc.):
Add the remote server to your MCP client:
```json
{
@ -22,36 +22,30 @@ Add this to your MCP client config (Claude, Cursor, Windsurf, VS Code, etc.):
}
```
The server requires **OAuth authentication**. Your MCP client will automatically discover the authorization server via `/.well-known/oauth-protected-resource` and prompt you to authenticate.
Supermemory MCP uses OAuth. Your client opens the authorization page so you can sign in and approve access. No API key or custom header is required.
## Project Scoping (Optional)
## Choose a space
To scope all operations to a specific project, add the `x-sm-project` header:
After connecting, space-aware tools use your active Supermemory space or account default. You can work in another space in two ways:
```json
{
"mcpServers": {
"supermemory": {
"url": "https://mcp.supermemory.ai/mcp",
"headers": {
"x-sm-project": "your-project-id"
}
}
}
}
```
- Name a space for a one-off action without changing your active space.
- Ask to switch your active space for future actions.
This keeps memories organized by project, useful when working on multiple codebases or contexts.
See [How spaces work](/supermemory-mcp/mcp#how-spaces-work) for examples and routing rules.
## Client-Specific Setup
## Client-specific setup
### Claude Desktop
### ChatGPT Web
For a screenshot-backed walkthrough (Settings → Connectors → Add custom connector with the URL above), see **[Claude Desktop](/supermemory-mcp/claude-desktop)**.
Enable developer mode, add Supermemory from the ChatGPT Plugins page, and complete OAuth. See the [ChatGPT Web guide](/supermemory-mcp/chatgpt-web) for the complete flow.
### Claude Desktop and Claude web
Open **Settings > Connectors**, add a custom connector, and enter the server URL. See the [Claude Desktop guide](/supermemory-mcp/claude-desktop) for the complete flow.
### Cursor
Add to `~/.cursor/mcp.json`:
Add the server to `~/.cursor/mcp.json`:
```json
{
@ -63,8 +57,22 @@ Add to `~/.cursor/mcp.json`:
}
```
Or use the one-click install button at [app.supermemory.ai](https://app.supermemory.ai).
You can also use the install option in [app.supermemory.ai](https://app.supermemory.ai).
### Windsurf / VS Code
### Other MCP clients
Configuration varies by extension. Generally, add the server URL (`https://mcp.supermemory.ai/mcp`) to your MCP settings.
Add `https://mcp.supermemory.ai/mcp` as a remote HTTP MCP server and complete the OAuth flow opened by your client. Exact menu names and configuration locations vary between clients.
Clients with MCP Apps support render the space picker, guided save, file upload, and memory graph directly in the conversation.
## Verify the connection
Start with any of these requests:
- "What Supermemory spaces can I access?"
- "What is my active Supermemory space?"
- "Search my saved context for the launch plan."
- "I want to upload a file to Supermemory."
- "Show my memory graph."
The [MCP overview](/supermemory-mcp/mcp) documents every tool, widget, resource, and the `context` prompt.

View file

@ -18,6 +18,7 @@ Everything in this section is one of four steps. Same loop whether you're buildi
<JourneyStep number="2" title="Ingest — get content in">
<JourneyItem icon="plus" title="Add memories & documents" href="/ingestion/add-memories" />
<JourneyItem icon="history" title="Backfill historical data" href="/ingestion/batch-ingest-historical-data" />
<JourneyItem icon="files" title="Document operations" href="/ingestion/document-operations" />
<JourneyItem icon="plug" title="Connectors" href="/connectors/overview" />
<JourneyItem icon="file-stack" title="Content types" href="/concepts/content-types" />

View file

@ -73,7 +73,7 @@ These tools are available to the embedded MCP App and hidden from the model.
| --- | --- |
| `set-active-tag` | Persist the selected active space |
| `save-memory` | Submit the guided save form |
| `upload-file-submit` | Submit an encoded file upload |
| `prepare-file-upload` | Prepare a secure direct file upload |
| `fetch-graph-data` | Fetch graph documents for the app |
## Resources And Prompt

View file

@ -16,12 +16,12 @@ const EXPECTED_TOOLS = [
"listMemories",
"listSpaces",
"memory-graph",
"prepare-file-upload",
"save-memory",
"search_memory",
"select-space",
"set-active-tag",
"upload-file",
"upload-file-submit",
"whoAmI",
]
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)

View file

@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import { graphResultMetaSchema, graphViewSchema } from "../src/shared/types"
import { graphViewSchema } from "../src/shared/types"
import {
OAUTH_CREDENTIALS_AVAILABLE,
callTool,
@ -29,8 +29,6 @@ describeWithAuth("MCP — graph, resources & prompts", () => {
expect(result.data.view).toBe("graph")
expect(result.data.rendered).toBe(true)
expect(result.data.documentCount).toBe(result.data.documents.length)
const resultMeta = graphResultMetaSchema.safeParse(res._meta)
expect(resultMeta.success).toBe(true)
})
it("fetch-graph-data returns paginated documents", async () => {

View file

@ -1,5 +1,9 @@
import { randomUUID } from "node:crypto"
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import {
uploadPreparationSchema,
uploadResponseSchema,
} from "../src/shared/types"
import {
OAUTH_CREDENTIALS_AVAILABLE,
callTool,
@ -153,29 +157,34 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
throw new Error("Upload did not provide a writable space")
}
const uploaded = await callTool(session.client, "upload-file-submit", {
fileData: Buffer.from(fileContent).toString("base64"),
fileName,
mimeType: "text/plain",
containerTag,
viewId: launcherView.viewId,
const prepared = await callTool(session.client, "prepare-file-upload")
expect(prepared.isError).toBeFalsy()
const preparation = uploadPreparationSchema.parse(
prepared.structuredContent,
)
const formData = new FormData()
formData.append("file", new Blob([fileContent]), fileName)
formData.append("containerTag", containerTag)
formData.append(
"metadata",
JSON.stringify({ sm_source: "supermemory-mcp" }),
)
const response = await fetch(preparation.uploadUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${preparation.uploadToken}`,
},
body: formData,
})
expect(uploaded.isError).toBeFalsy()
const uploadedView = uploaded.structuredContent as AppView
expect(uploadedView).toMatchObject({
view: "upload-success",
fileName,
containerTag,
})
expect(uploadedView.id).toBeTruthy()
if (!uploadedView.id)
throw new Error("Upload did not return a document ID")
expect(response.ok).toBe(true)
const uploaded = uploadResponseSchema.parse(await response.json())
const document = await waitForToolText(
session,
"getDocument",
{ documentId: uploadedView.id },
`Document ID: ${uploadedView.id}`,
{ documentId: uploaded.id },
`Document ID: ${uploaded.id}`,
20,
1000,
)

View file

@ -1,219 +0,0 @@
import { describe, expect, it } from "vitest"
import type {
DocumentDetails,
DocumentsListResponse,
MemoryEntriesResponse,
} from "./server/client"
import {
formatDocument,
formatDocumentsList,
formatMemoryEntriesList,
} from "./server/format"
function makeDocumentsResponse(
overrides: Partial<DocumentsListResponse> = {},
): DocumentsListResponse {
return {
documents: [],
pagination: { currentPage: 1, limit: 10, totalItems: 0, totalPages: 1 },
...overrides,
}
}
function makeMemoryResponse(
overrides: Partial<MemoryEntriesResponse> = {},
): MemoryEntriesResponse {
return {
memoryEntries: [],
pagination: { currentPage: 1, limit: 10, totalItems: 0, totalPages: 1 },
...overrides,
}
}
function makeMemory(memory: string, extra: Record<string, unknown> = {}) {
return {
id: `mem_${memory.slice(0, 8)}`,
memory,
version: 1,
isLatest: true,
isForgotten: false,
createdAt: "2026-06-10T12:00:00Z",
updatedAt: "2026-06-10T12:00:00Z",
...extra,
}
}
describe("formatDocumentsList", () => {
it("reports an empty document store", () => {
expect(formatDocumentsList(makeDocumentsResponse())).toBe(
"No documents stored yet.",
)
})
it("formats document metadata and stable IDs without content", () => {
const result = formatDocumentsList(
makeDocumentsResponse({
documents: [
{
id: "doc_1",
connectionId: null,
createdAt: "2026-06-12T08:00:00Z",
customId: null,
metadata: null,
status: "done",
summary: "A compact summary.",
title: "Preferences",
type: "text",
updatedAt: "2026-06-12T08:00:00Z",
},
],
pagination: { currentPage: 1, limit: 10, totalItems: 1, totalPages: 1 },
}),
)
expect(result).toContain(
"1 document (page 1 of 1, 1 document total), newest first.",
)
expect(result).toContain('- [doc_1] "Preferences" (text, done, 2026-06-12)')
expect(result).toContain("Summary: A compact summary.")
expect(result).toContain(
"Use getDocument with a document ID to read its content.",
)
})
it("points to the next document page", () => {
const result = formatDocumentsList(
makeDocumentsResponse({
documents: [
{
id: "doc_1",
connectionId: null,
createdAt: "2026-06-12T08:00:00Z",
customId: null,
metadata: null,
status: "done",
summary: null,
title: null,
type: "text",
updatedAt: "2026-06-12T08:00:00Z",
},
],
pagination: { currentPage: 1, limit: 1, totalItems: 3, totalPages: 3 },
}),
)
expect(result).toContain('"(untitled)"')
expect(result).toContain(
"More available - call listDocuments with page: 2.",
)
})
})
describe("formatMemoryEntriesList", () => {
it("reports an empty memory store", () => {
expect(formatMemoryEntriesList(makeMemoryResponse())).toBe(
"No active memories stored yet.",
)
})
it("formats active memories independently of documents", () => {
const result = formatMemoryEntriesList(
makeMemoryResponse({
memoryEntries: [
makeMemory("User prefers dark mode", {
id: "mem_1",
version: 2,
documentIds: ["doc_1", "doc_2"],
history: [
{
id: "mem_old",
memory: "User sometimes uses dark mode",
version: 1,
createdAt: "2026-06-01T00:00:00Z",
updatedAt: "2026-06-01T00:00:00Z",
},
],
}),
],
pagination: { currentPage: 1, limit: 10, totalItems: 1, totalPages: 1 },
}),
)
expect(result).toContain(
"1 active memory (page 1 of 1, 1 memory entry total), newest first.",
)
expect(result).toContain("- [mem_1] User prefers dark mode")
expect(result).toContain(
"version 2 | updated 2026-06-10 | 1 previous version",
)
expect(result).toContain("Source documents: doc_1, doc_2")
})
it("excludes forgotten and superseded entries", () => {
const result = formatMemoryEntriesList(
makeMemoryResponse({
memoryEntries: [
makeMemory("Current fact"),
makeMemory("Forgotten fact", { isForgotten: true }),
makeMemory("Old fact", { isLatest: false }),
],
pagination: { currentPage: 1, limit: 10, totalItems: 3, totalPages: 1 },
}),
)
expect(result).toContain("Current fact")
expect(result).not.toContain("Forgotten fact")
expect(result).not.toContain("Old fact")
})
it("flattens and truncates oversized memory text", () => {
const result = formatMemoryEntriesList(
makeMemoryResponse({
memoryEntries: [
makeMemory("line one\nline two"),
makeMemory(`start ${"x".repeat(600)}`),
],
pagination: { currentPage: 1, limit: 10, totalItems: 2, totalPages: 1 },
}),
)
expect(result).toContain("line one line two")
expect(result).toContain("... [truncated]")
})
})
describe("formatDocument", () => {
const document: DocumentDetails = {
id: "doc_1",
connectionId: null,
content: "Original input",
createdAt: "2026-06-12T08:00:00Z",
customId: null,
metadata: null,
ogImage: null,
raw: "Full extracted document text",
source: "text",
spatialPoint: null,
status: "done",
summary: "A compact summary.",
title: "Preferences",
type: "text",
updatedAt: "2026-06-12T09:00:00Z",
url: null,
}
it("returns document metadata, summary, and full available content", () => {
const result = formatDocument(document)
expect(result).toContain("# Preferences")
expect(result).toContain("Document ID: doc_1")
expect(result).toContain("## Summary\nA compact summary.")
expect(result).toContain("## Content\nFull extracted document text")
expect(result).not.toContain("Original input")
})
it("falls back to the original content when raw content is absent", () => {
const result = formatDocument({ ...document, raw: null })
expect(result).toContain("## Content\nOriginal input")
})
})

View file

@ -46,7 +46,7 @@ const TOOL_SURFACES: Record<string, McpToolSurface> = {
"upload-file": "app_launcher",
"set-active-tag": "app_action",
"save-memory": "app_action",
"upload-file-submit": "app_action",
"prepare-file-upload": "app_action",
"fetch-graph-data": "app_internal",
}

View file

@ -1,33 +0,0 @@
import { describe, expect, it } from "vitest"
import {
appResultMeta,
appToolMeta,
SUPERMEMORY_RESOURCE_URI,
} from "./app-metadata"
describe("MCP Apps metadata compatibility", () => {
it("advertises both current and legacy resource URI metadata", () => {
expect(SUPERMEMORY_RESOURCE_URI).toMatch(
/^ui:\/\/supermemory\/app-[a-f0-9]{64}\.html$/,
)
expect(appToolMeta()).toEqual({
ui: { resourceUri: SUPERMEMORY_RESOURCE_URI },
"ui/resourceUri": SUPERMEMORY_RESOURCE_URI,
})
})
it("keeps App-only tools hidden from the model", () => {
expect(appToolMeta(["app"])).toMatchObject({
ui: {
resourceUri: SUPERMEMORY_RESOURCE_URI,
visibility: ["app"],
},
})
})
it("provides a stable ChatGPT widget-state key", () => {
expect(appResultMeta("view-123")).toEqual({
"openai/widgetSessionId": "view-123",
})
})
})

View file

@ -1,6 +1,11 @@
import { createLocalJWKSet, exportJWK, generateKeyPair, SignJWT } from "jose"
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"
import { fetchSession, validateOAuthToken } from "./index"
import {
fetchSession,
TransientAuthError,
validateApiKey,
validateOAuthToken,
} from "./index"
const API_URL = "https://api.example.com"
const ISSUER = `${API_URL}/api/auth`
@ -120,4 +125,92 @@ describe("MCP authentication", () => {
status: 403,
})
})
function sessionResponse() {
return Response.json({
user: { id: "user_test", email: "test@example.com" },
org: { id: "org_test" },
role: "owner",
accessType: "full",
scope: { type: "full", permission: "write" },
})
}
it("validates an sm_ API key via the session endpoint", async () => {
const fetchSpy = vi.fn().mockResolvedValue(sessionResponse())
vi.stubGlobal("fetch", fetchSpy)
const key = "sm_valid_key_0123456789abcdef"
await expect(validateApiKey(key, API_URL)).resolves.toEqual({
userId: "user_test",
organizationId: "org_test",
bearerToken: key,
scopes: [],
})
expect(fetchSpy).toHaveBeenCalledWith(
`${API_URL}/v3/session`,
expect.objectContaining({
headers: { Authorization: `Bearer ${key}` },
}),
)
})
it("caches a validated API key within the TTL", async () => {
const fetchSpy = vi.fn().mockResolvedValue(sessionResponse())
vi.stubGlobal("fetch", fetchSpy)
const key = "sm_cached_key_0123456789abcdef"
await validateApiKey(key, API_URL)
await validateApiKey(key, API_URL)
expect(fetchSpy).toHaveBeenCalledTimes(1)
})
it("rejects an API key the session endpoint refuses", async () => {
vi.spyOn(console, "error").mockImplementation(() => {})
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(new Response(null, { status: 401 })),
)
await expect(
validateApiKey("sm_revoked_key_0123456789abcdef", API_URL),
).resolves.toBeNull()
})
it("rejects malformed API keys without an API request", async () => {
const fetchSpy = vi.fn()
vi.stubGlobal("fetch", fetchSpy)
await expect(validateApiKey("sm_short", API_URL)).resolves.toBeNull()
await expect(validateApiKey("not_a_key", API_URL)).resolves.toBeNull()
expect(fetchSpy).not.toHaveBeenCalled()
})
it("surfaces a 500 from the session endpoint as TransientAuthError, not invalid token", async () => {
vi.spyOn(console, "error").mockImplementation(() => {})
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(new Response(null, { status: 500 })),
)
await expect(
validateApiKey("sm_outage_key_0123456789abcdef", API_URL),
).rejects.toThrow(TransientAuthError)
})
it("surfaces a session-endpoint timeout as TransientAuthError", async () => {
vi.spyOn(console, "error").mockImplementation(() => {})
vi.stubGlobal(
"fetch",
vi.fn().mockRejectedValue(
Object.assign(new Error("The operation was aborted"), {
name: "TimeoutError",
}),
),
)
await expect(
validateApiKey("sm_timeout_key_0123456789abcd", API_URL),
).rejects.toThrow(TransientAuthError)
})
})

View file

@ -52,6 +52,93 @@ export async function fetchSession(
return result.data
}
// Opaque Supermemory API keys (sm_...) authenticate via the session endpoint
// instead of JWT verification. Successful lookups are cached per isolate so a
// busy MCP session doesn't re-validate on every JSON-RPC message.
const API_KEY_PATTERN = /^sm_\S{17,}$/
const API_KEY_CACHE_TTL_MS = 60_000
const API_KEY_CACHE_MAX_ENTRIES = 1000
const apiKeyCache = new Map<string, { user: AuthUser; expiresAt: number }>()
export function isApiKey(token: string): boolean {
return API_KEY_PATTERN.test(token)
}
// Upstream was unreachable, not the token being bad: reporting these as invalid_token makes clients discard working credentials.
export class TransientAuthError extends Error {
readonly status?: number
constructor(message: string, status?: number) {
super(message)
this.name = "TransientAuthError"
this.status = status
}
}
const TRANSIENT_ERROR_NAMES = new Set([
"AbortError",
"TimeoutError",
"JWKSTimeout",
])
// ERR_JOSE_GENERIC is what jose throws when the JWKS endpoint answers non-200 or unparseable JSON.
const TRANSIENT_JOSE_CODES = new Set(["ERR_JWKS_TIMEOUT", "ERR_JOSE_GENERIC"])
function transientAuthErrorFor(error: unknown): TransientAuthError | null {
const status = (error as { status?: unknown } | null)?.status
if (typeof status === "number" && status !== 401 && status !== 403) {
return new TransientAuthError(`Session endpoint returned ${status}`, status)
}
if (error instanceof TypeError) {
return new TransientAuthError(`Auth backend unreachable: ${error.message}`)
}
if (error instanceof Error && TRANSIENT_ERROR_NAMES.has(error.name)) {
return new TransientAuthError(error.message)
}
const code = (error as { code?: unknown } | null)?.code
if (typeof code === "string" && TRANSIENT_JOSE_CODES.has(code)) {
return new TransientAuthError(
`JWKS fetch failed: ${(error as Error).message}`,
)
}
return null
}
export async function validateApiKey(
token: string,
apiUrl: string,
): Promise<AuthUser | null> {
if (!isApiKey(token)) return null
const cached = apiKeyCache.get(token)
if (cached && cached.expiresAt > Date.now()) return cached.user
try {
const session = await fetchSession(token, apiUrl)
const organizationId = session.org?.id
if (!organizationId) return null
const user: AuthUser = {
userId: session.user.id,
organizationId,
bearerToken: token,
scopes: [],
}
if (apiKeyCache.size >= API_KEY_CACHE_MAX_ENTRIES) apiKeyCache.clear()
apiKeyCache.set(token, {
user,
expiresAt: Date.now() + API_KEY_CACHE_TTL_MS,
})
return user
} catch (error) {
console.error("API key validation error:", error)
const transient = transientAuthErrorFor(error)
if (transient) throw transient
return null
}
}
export async function validateOAuthToken(
token: string,
apiUrl: string,
@ -97,6 +184,8 @@ export async function validateOAuthToken(
}
} catch (error) {
console.error("OAuth token validation error:", error)
const transient = transientAuthErrorFor(error)
if (transient) throw transient
return null
}
}

View file

@ -1,63 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest"
import { SupermemoryClient } from "."
describe("SupermemoryClient memory listing", () => {
afterEach(() => {
vi.unstubAllGlobals()
})
it("calls the canonical memory-list endpoint with the selected space", async () => {
const responseBody = {
memoryEntries: [
{
id: "mem_1",
memory: "User prefers dark mode",
version: 1,
isLatest: true,
isForgotten: false,
createdAt: "2026-07-29T00:00:00.000Z",
updatedAt: "2026-07-29T00:00:00.000Z",
history: [],
documentIds: ["doc_1"],
},
],
pagination: {
currentPage: 2,
limit: 20,
totalItems: 21,
totalPages: 2,
},
}
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify(responseBody), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
)
vi.stubGlobal("fetch", fetchMock)
const client = new SupermemoryClient(
"oauth-token",
"snowcone_grande",
"https://api.example.com",
)
await expect(client.listMemoryEntries(2, 20)).resolves.toEqual(responseBody)
expect(fetchMock).toHaveBeenCalledOnce()
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe("https://api.example.com/v4/memories/list")
expect(init.method).toBe("POST")
expect(init.headers).toMatchObject({
Authorization: "Bearer oauth-token",
"Content-Type": "application/json",
"x-sm-source": "supermemory-mcp",
})
expect(JSON.parse(init.body as string)).toEqual({
containerTags: ["snowcone_grande"],
page: 2,
limit: 20,
sort: "createdAt",
order: "desc",
})
})
})

View file

@ -7,11 +7,14 @@ import { z } from "zod"
import {
containerTagSchema,
documentsApiResponseSchema,
paginationSchema,
memoriesListSchema,
type ContainerTag,
type DocumentMemoryEntry,
type DocumentsApiResponse,
type DocumentWithMemories,
type MemoriesList,
type MemoryEntry,
type MemoryEntryHistory,
} from "../../shared/types"
const MAX_CHARS = 200000
@ -34,43 +37,10 @@ export interface DocumentsListResponse {
pagination: SdkDocumentListResponse["pagination"]
}
const memoryEntryHistorySchema = z.looseObject({
id: z.string(),
memory: z.string(),
version: z.number(),
createdAt: z.string(),
updatedAt: z.string(),
parentMemoryId: z.string().nullish(),
rootMemoryId: z.string().nullish(),
isLatest: z.boolean().optional(),
isForgotten: z.boolean().optional(),
})
export type MemoryEntryHistory = z.infer<typeof memoryEntryHistorySchema>
const memoryEntrySchema = z.looseObject({
id: z.string(),
memory: z.string(),
version: z.number(),
isLatest: z.boolean(),
isForgotten: z.boolean(),
isStatic: z.boolean().optional(),
isInference: z.boolean().optional(),
createdAt: z.string(),
updatedAt: z.string(),
sourceCount: z.number().optional(),
documentIds: z.array(z.string()).optional(),
history: z.array(memoryEntryHistorySchema).optional(),
})
export type MemoryEntry = z.infer<typeof memoryEntrySchema>
const memoryEntriesResponseSchema = z.object({
memoryEntries: z.array(memoryEntrySchema),
pagination: paginationSchema,
})
export type MemoryEntriesResponse = z.infer<typeof memoryEntriesResponseSchema>
// Memory-entry shapes live in shared/types so the client parser and the
// listMemories output schema share one definition and can't drift.
export type { MemoryEntry, MemoryEntryHistory }
export type MemoryEntriesResponse = MemoriesList
export type Memory =
| {
@ -122,11 +92,6 @@ const sdkResultSchema = z.looseObject({
context: z.string().nullish(),
})
const uploadResultSchema = z.object({
id: z.string(),
status: z.string(),
})
function mapSdkResults(value: unknown): Memory[] {
return z
.array(sdkResultSchema)
@ -154,6 +119,19 @@ function objectProperty(value: unknown, key: string): unknown {
: undefined
}
// API error bodies are JSON like {"error": "..."} — unwrap them so users see
// the real reason instead of raw JSON or a generic fallback.
function extractApiErrorMessage(raw: unknown): string | undefined {
if (typeof raw !== "string" || !raw) return undefined
try {
const parsed = JSON.parse(raw) as { error?: unknown; message?: unknown }
if (typeof parsed.error === "string" && parsed.error) return parsed.error
if (typeof parsed.message === "string" && parsed.message)
return parsed.message
} catch {}
return raw
}
export class SupermemoryClient {
private client: Supermemory
private containerTag: string
@ -376,7 +354,8 @@ export class SupermemoryClient {
signal,
})
if (!response.ok) {
throw Object.assign(new Error("Failed to fetch documents"), {
const message = extractApiErrorMessage(await response.text())
throw Object.assign(new Error(message ?? ""), {
status: response.status,
})
}
@ -437,51 +416,13 @@ export class SupermemoryClient {
})
if (!response.ok) {
const message = await response.text()
throw Object.assign(
new Error(message || "Failed to fetch memory entries"),
{ status: response.status },
)
}
return memoryEntriesResponseSchema.parse(await response.json())
} catch (error) {
this.handleError(error)
}
}
async uploadFile(
fileData: ArrayBuffer,
fileName: string,
mimeType: string,
containerTag?: string,
): Promise<{ id: string; status: string }> {
try {
const formData = new FormData()
const blob = new Blob([fileData], { type: mimeType })
formData.append("file", blob, fileName)
if (containerTag) {
formData.append("containerTags", containerTag)
}
formData.append("metadata", JSON.stringify({ sm_source: MCP_SOURCE }))
const response = await fetch(`${this.apiUrl}/v3/documents/file`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.bearerToken}`,
"x-sm-source": MCP_SOURCE,
},
body: formData,
})
if (!response.ok) {
const text = await response.text()
throw Object.assign(new Error(text || "Upload failed"), {
const message = extractApiErrorMessage(await response.text())
throw Object.assign(new Error(message ?? ""), {
status: response.status,
})
}
return uploadResultSchema.parse(await response.json())
return memoriesListSchema.parse(await response.json())
} catch (error) {
this.handleError(error)
}
@ -508,8 +449,7 @@ export class SupermemoryClient {
const status = objectProperty(error, "status")
if (typeof status === "number") {
const rawMessage = objectProperty(error, "message")
const message = typeof rawMessage === "string" ? rawMessage : undefined
const message = extractApiErrorMessage(objectProperty(error, "message"))
switch (status) {
case 400:
case 422:
@ -521,7 +461,7 @@ export class SupermemoryClient {
case 403:
throw new Error(
message ||
"Access forbidden. Your account may be restricted or blocked.",
"Access forbidden. This connection may be read-only or scoped to specific spaces — reconnect with broader access, or check your account status.",
)
case 404:
throw new Error("Not found.")

View file

@ -2,11 +2,17 @@ import type { AuthInfo } from "@modelcontextprotocol/server"
import { createMcpHandler } from "agents/mcp/server"
import { Hono, type Context } from "hono"
import { cors } from "hono/cors"
import { validateOAuthToken, type AuthUser } from "./auth"
import {
isApiKey,
TransientAuthError,
validateApiKey,
validateOAuthToken,
type AuthUser,
} from "./auth"
import { SupermemoryMCP } from "./legacy-protocol-state"
import { createSupermemoryServer } from "./server"
import type { ActorContext, ServerEnv } from "./types"
import { SpaceState } from "./space-state"
import { SpaceState, uploadStateName } from "./space-state"
type Bindings = ServerEnv
@ -16,6 +22,8 @@ const DEFAULT_API_URL = "https://api.supermemory.ai"
const DEFAULT_MCP_RESOURCE = "https://mcp.supermemory.ai/mcp"
const PROTECTED_RESOURCE_METADATA_PATH =
"/.well-known/oauth-protected-resource/mcp"
const UPLOAD_ID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
const DEFAULT_ALLOWED_ORIGIN_HOSTNAMES = [
"app.supermemory.ai",
"mcp.supermemory.ai",
@ -40,7 +48,7 @@ app.use(
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
// When omitted, Hono echoes Access-Control-Request-Headers. This keeps
// modern Mcp-Method/Mcp-Name/Mcp-Param-* routing forward-compatible.
exposeHeaders: ["WWW-Authenticate"],
exposeHeaders: ["WWW-Authenticate", "Retry-After"],
}),
)
@ -121,6 +129,30 @@ function authInfoFor(
}
}
type AuthResolution =
| { ok: true; user: AuthUser }
| { ok: false; reason: "invalid" }
| { ok: false; reason: "transient" }
// Keeps a transient upstream failure distinct from an invalid token.
async function resolveAuthUser(
token: string,
apiUrl: string,
mcpResource: string,
): Promise<AuthResolution> {
try {
const user = isApiKey(token)
? await validateApiKey(token, apiUrl)
: await validateOAuthToken(token, apiUrl, mcpResource)
return user ? { ok: true, user } : { ok: false, reason: "invalid" }
} catch (error) {
if (error instanceof TransientAuthError) {
return { ok: false, reason: "transient" }
}
throw error
}
}
function unauthorizedResponse(
resourceMetadataUrl: string,
invalidToken = false,
@ -170,11 +202,27 @@ async function handleMcpRequest(
const resourceMetadataUrl = reqHost
? `${reqProto}://${reqHost}${PROTECTED_RESOURCE_METADATA_PATH}`
: PROTECTED_RESOURCE_METADATA_PATH
const mcpOrigin = c.env.MCP_PUBLIC_ORIGIN || new URL(mcpResource).origin
if (!token) return unauthorizedResponse(resourceMetadataUrl)
const authUser = await validateOAuthToken(token, apiUrl, mcpResource)
if (!authUser) return unauthorizedResponse(resourceMetadataUrl, true)
const resolved = await resolveAuthUser(token, apiUrl, mcpResource)
if (!resolved.ok && resolved.reason === "transient") {
return Response.json(
{
jsonrpc: "2.0",
error: {
code: -32001,
message:
"Authentication backend temporarily unavailable, please retry",
},
id: null,
},
{ status: 503, headers: { "Retry-After": "5" } },
)
}
if (!resolved.ok) return unauthorizedResponse(resourceMetadataUrl, true)
const authUser = resolved.user
const actor: ActorContext = {
userId: authUser.userId,
@ -187,8 +235,11 @@ async function handleMcpRequest(
: c.req.raw
const handler = createMcpHandler(
() =>
createSupermemoryServer(c.env, actor, (promise) =>
c.executionCtx.waitUntil(promise),
createSupermemoryServer(
c.env,
actor,
(promise) => c.executionCtx.waitUntil(promise),
mcpOrigin,
),
{
route: "/mcp",
@ -204,6 +255,56 @@ async function handleMcpRequest(
})
}
app.post("/upload/:uploadId", async (c) => {
const uploadId = c.req.param("uploadId")
const contentType = c.req.header("Content-Type")
const authHeader = c.req.header("Authorization")
const uploadToken = authHeader?.replace(/^Bearer\s+/i, "").trim()
if (!UPLOAD_ID_PATTERN.test(uploadId) || !uploadToken) {
return c.json({ error: "Invalid or expired upload session" }, 401)
}
if (!contentType?.toLowerCase().startsWith("multipart/form-data;")) {
return c.json({ error: "Expected multipart form data" }, 415)
}
if (!c.req.raw.body) {
return c.json({ error: "File upload body is required" }, 400)
}
const uploadState = c.env.SPACE_STATE.getByName(uploadStateName(uploadId))
const session = await uploadState.consumeUploadSession(uploadToken)
if (!session) {
return c.json({ error: "Invalid or expired upload session" }, 401)
}
const apiUrl = (c.env.API_URL || DEFAULT_API_URL).replace(/\/+$/, "")
try {
const response = await fetch(`${apiUrl}/v3/documents/file`, {
method: "POST",
headers: {
Authorization: `Bearer ${session.bearerToken}`,
"Content-Type": contentType,
"x-sm-source": "supermemory-mcp",
},
body: c.req.raw.body,
signal: c.req.raw.signal,
})
const headers = new Headers({ "Cache-Control": "no-store" })
const responseContentType = response.headers.get("Content-Type")
const retryAfter = response.headers.get("Retry-After")
if (responseContentType) headers.set("Content-Type", responseContentType)
if (retryAfter) headers.set("Retry-After", retryAfter)
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
})
} catch {
return c.json({ error: "File upload failed" }, 502)
}
})
app.all("/", (c) => handleMcpRequest(c, "/mcp"))
app.all("/mcp", (c) => handleMcpRequest(c))
app.all("/mcp/", (c) => handleMcpRequest(c, "/mcp"))

View file

@ -1,4 +1,4 @@
import type { McpServer } from "@modelcontextprotocol/server"
import { ResourceTemplate, type McpServer } from "@modelcontextprotocol/server"
import supermemoryAppHtml from "../../../dist/src/widget/index.html"
import {
APP_RESOURCE_MIME_TYPE,
@ -6,33 +6,42 @@ import {
} from "../app-metadata"
import {
WIDGET_DESCRIPTION,
WIDGET_RESOURCE_META,
widgetResourceMeta,
} from "../widget-resource-metadata"
export function registerWidgetResource(server: McpServer) {
export function registerWidgetResource(
server: McpServer,
connectOrigin: string,
) {
const resourceMeta = widgetResourceMeta(connectOrigin)
const resourceConfig = {
mimeType: APP_RESOURCE_MIME_TYPE,
description: WIDGET_DESCRIPTION,
_meta: resourceMeta,
}
const readWidgetResource = (uri: string) => ({
contents: [
{
uri,
mimeType: APP_RESOURCE_MIME_TYPE,
text: supermemoryAppHtml,
_meta: resourceMeta,
},
],
})
server.registerResource(
"Supermemory MCP UI",
SUPERMEMORY_RESOURCE_URI,
// Listing-level metadata: hosts use this when discovering resources
// before invoking the read callback. Mirrors the read response below
// so prefetch/connect-time decisions match what the host will get.
{
mimeType: APP_RESOURCE_MIME_TYPE,
description: WIDGET_DESCRIPTION,
_meta: WIDGET_RESOURCE_META,
},
// Read response: per spec, content-item `_meta.ui` takes precedence
// over the listing-level value. Set both to the same object so behavior
// is consistent regardless of which path the host inspects.
async () => ({
contents: [
{
uri: SUPERMEMORY_RESOURCE_URI,
mimeType: APP_RESOURCE_MIME_TYPE,
text: supermemoryAppHtml,
_meta: WIDGET_RESOURCE_META,
},
],
resourceConfig,
async () => readWidgetResource(SUPERMEMORY_RESOURCE_URI),
)
server.registerResource(
"Supermemory MCP UI compatibility",
new ResourceTemplate("ui://supermemory/app-{version}.html", {
list: undefined,
}),
resourceConfig,
async (uri) => readWidgetResource(uri.href),
)
}

View file

@ -21,8 +21,10 @@ import {
resolveContainerTag as resolveSpaceContainerTag,
spaceStateName,
} from "./space"
import { uploadStateName } from "./space-state"
const DEFAULT_API_URL = "https://api.supermemory.ai"
const UPLOAD_SESSION_TTL_MS = 2 * 60 * 1000
const SERVER_INSTRUCTIONS =
"Supermemory is the authenticated user's persistent memory and knowledge layer across conversations and spaces. Use these tools whenever the user wants to recall something they may have saved, inspect stored sources or extracted memories, remember or upload new information, check their Supermemory account or access, change their active space, or explore their memory graph, even if they do not mention Supermemory by name. Use the active or account-default space when none is named. Resolve a named space with listSpaces and pass its key to the relevant tool; change the active space only when the user explicitly asks."
@ -48,6 +50,7 @@ export function createSupermemoryServer(
env: ServerEnv,
actor: ActorContext,
waitUntil: WaitUntil,
mcpOrigin: string,
): McpServer {
const server = new McpServer(
{
@ -68,6 +71,23 @@ export function createSupermemoryServer(
resolveSpaceContainerTag(explicit, getActiveContainerTag)
const resolveContainerTag = async (explicit?: string) =>
(await resolveSelectedContainerTag(explicit)) ?? DEFAULT_PROJECT_ID
const createUploadSession = async () => {
const uploadId = crypto.randomUUID()
const uploadToken = [crypto.randomUUID(), crypto.randomUUID()]
.join("")
.replaceAll("-", "")
const expiresAt = Date.now() + UPLOAD_SESSION_TTL_MS
const uploadState = env.SPACE_STATE.getByName(uploadStateName(uploadId))
await uploadState.createUploadSession(uploadToken, {
bearerToken: actor.bearerToken,
expiresAt,
})
return {
uploadUrl: new URL(`/upload/${uploadId}`, mcpOrigin).toString(),
uploadToken,
expiresAt,
}
}
const analytics = createPosthogAnalytics(env, actor, waitUntil)
const toolServer = createTrackedToolServer(
server,
@ -83,6 +103,7 @@ export function createSupermemoryServer(
resolveContainerTag,
getActiveContainerTag,
setActiveContainerTag,
createUploadSession,
getClientInfo: clientInfoFromContext,
errorResult,
})
@ -93,7 +114,7 @@ export function createSupermemoryServer(
() => getClient(),
resolveSelectedContainerTag,
)
registerWidgetResource(server)
registerWidgetResource(server, mcpOrigin)
registerContextPrompt(server, getClient, resolveSelectedContainerTag)
return server

View file

@ -1,64 +0,0 @@
import { describe, expect, it } from "vitest"
import type { ContainerTag } from "../shared/types"
import {
compactDescription,
formatFactSection,
formatSpaceRow,
sortSpaces,
} from "./space-presentation"
const space = (
containerTag: string,
lastActivityAt: string | null,
): ContainerTag => ({
id: containerTag,
name: `Space ${containerTag}`,
containerTag,
description: "A compact space description.",
visibility: "private",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
isExperimental: false,
isNova: false,
documentCount: 2,
memoryCount: 3,
lastActivityAt,
})
describe("space presentation", () => {
it("keeps the active space first, then sorts by activity", () => {
const sorted = sortSpaces(
[
space("older", "2026-01-01T00:00:00.000Z"),
space("active", "2025-01-01T00:00:00.000Z"),
space("newer", "2026-02-01T00:00:00.000Z"),
],
"active",
)
expect(sorted.map((item) => item.containerTag)).toEqual([
"active",
"newer",
"older",
])
})
it("formats compact rows without internal database IDs", () => {
const row = formatSpaceRow(
space("project-key", "2026-07-29T19:44:28.177Z"),
"project-key",
)
expect(row).toContain("[project-key] · Active")
expect(row).toContain("2 documents · 3 memories")
expect(row).toContain("Last active Jul 29, 2026")
expect(row).not.toContain('"id"')
})
it("caps descriptions and fact lists", () => {
expect(compactDescription("A".repeat(30), 12)).toBe("AAAAAAAAA...")
expect(
formatFactSection("Recent Context", ["one", "two", "three"], 2),
).toEqual(["## Recent Context", "- one", "- two", "- +1 more"])
})
})

View file

@ -2,6 +2,26 @@ import { DurableObject } from "cloudflare:workers"
import { containerTagSchema } from "./container-tag"
const ACTIVE_CONTAINER_TAG_KEY = "activeContainerTag"
const UPLOAD_SESSION_KEY = "uploadSession"
export interface UploadSession {
bearerToken: string
expiresAt: number
}
async function hashUploadToken(token: string): Promise<string> {
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(token),
)
return Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("")
}
export function uploadStateName(uploadId: string): string {
return `upload:${uploadId}`
}
export class SpaceState extends DurableObject {
async getActiveContainerTag(): Promise<string | undefined> {
@ -12,4 +32,42 @@ export class SpaceState extends DurableObject {
const validatedTag = containerTagSchema.parse(containerTag)
await this.ctx.storage.put(ACTIVE_CONTAINER_TAG_KEY, validatedTag)
}
async createUploadSession(
token: string,
session: UploadSession,
): Promise<void> {
await this.ctx.storage.put(UPLOAD_SESSION_KEY, {
...session,
tokenHash: await hashUploadToken(token),
})
await this.ctx.storage.setAlarm(session.expiresAt)
}
async consumeUploadSession(
token: string,
): Promise<UploadSession | undefined> {
const tokenHash = await hashUploadToken(token)
return this.ctx.storage.transaction(async (transaction) => {
const session = await transaction.get<
UploadSession & { tokenHash: string }
>(UPLOAD_SESSION_KEY)
if (!session) return undefined
if (session.expiresAt <= Date.now()) {
await transaction.delete(UPLOAD_SESSION_KEY)
return undefined
}
if (session.tokenHash !== tokenHash) return undefined
await transaction.delete(UPLOAD_SESSION_KEY)
return {
bearerToken: session.bearerToken,
expiresAt: session.expiresAt,
}
})
}
async alarm(): Promise<void> {
await this.ctx.storage.delete(UPLOAD_SESSION_KEY)
}
}

View file

@ -12,8 +12,8 @@ export function register(deps: ToolDeps) {
description: "Fetch documents with memories for graph display",
inputSchema: z.object({
containerTag: optionalContainerTagSchema,
page: z.number().optional().default(1),
limit: z.number().optional().default(200),
page: z.number().int().min(1).max(10_000).optional().default(1),
limit: z.number().int().min(1).max(1_000).optional().default(200),
}),
outputSchema: documentsApiResponseSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,

View file

@ -21,7 +21,7 @@ export function register(deps: ToolDeps) {
{
title: "Get Document",
description:
"Read one stored document by ID, including its summary and available content. Use listDocuments in the intended space to discover document IDs.",
"Read one stored document by ID from any space you can access, including its summary and available content. Use listDocuments to discover document IDs.",
inputSchema,
outputSchema: getDocumentOutputSchema,
annotations: READ_ONLY_TOOL_ANNOTATIONS,

Some files were not shown because too many files have changed in this diff Show more