Merge remote-tracking branch 'origin/main' into fix/docs-broken-links-ci
257
.github/workflows/ci-python.yml
vendored
Normal 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)"
|
||||
79
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
.github/workflows/claude-code-review.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
53
.github/workflows/publish-ai-sdk.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
10
.github/workflows/publish-pipecat-sdk-python.yml
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
1
apps/docs/images/cursor-logo.svg
Normal 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 |
BIN
apps/docs/images/grok-logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 49 KiB |
BIN
apps/docs/images/supermemory-mcp/chatgpt-web/faq-memory-dark.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 689 KiB |
|
After Width: | Height: | Size: 667 KiB |
BIN
apps/docs/images/supermemory-mcp/widgets/guided-save-dark.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
apps/docs/images/supermemory-mcp/widgets/guided-save-draft.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
apps/docs/images/supermemory-mcp/widgets/memory-graph-dark.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
apps/docs/images/supermemory-mcp/widgets/memory-graph.png
Normal file
|
After Width: | Height: | Size: 383 KiB |
BIN
apps/docs/images/supermemory-mcp/widgets/select-space-dark.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
apps/docs/images/supermemory-mcp/widgets/select-space.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
apps/docs/images/supermemory-mcp/widgets/upload-file-dark.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
apps/docs/images/supermemory-mcp/widgets/upload-file.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
145
apps/docs/ingestion/batch-ingest-historical-data.mdx
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
208
apps/docs/integrations/cursor.mdx
Normal 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>
|
||||
64
apps/docs/integrations/grok-bot.mdx
Normal 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>
|
||||
|
|
@ -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) |
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
245
apps/docs/supermemory-mcp/chatgpt-web.mdx
Normal 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.
|
||||
|
||||

|
||||
</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**.
|
||||
|
||||

|
||||
|
||||
## 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>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||