Compare commits

..

No commits in common. "main" and "server-v0.0.6" have entirely different histories.

871 changed files with 107907 additions and 30622 deletions

View file

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

View file

@ -26,83 +26,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: bun install --frozen-lockfile
- name: Detect SDK and playground changes - name: Run TypeScript type checking
id: sdk-changes run: bunx turbo run check-types --filter='@supermemory/ai-sdk' --filter='@supermemory/memory-graph'
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) - name: Run Biome CI (format & lint on changed files)
run: bunx biome ci --changed --since=origin/main --no-errors-on-unmatched run: bunx biome ci --changed --since=origin/main --no-errors-on-unmatched

View file

@ -77,6 +77,8 @@ jobs:
Branch: ${{ github.event.workflow_run.head_branch }} Branch: ${{ github.event.workflow_run.head_branch }}
Repository: ${{ github.repository }} Repository: ${{ github.repository }}
Check supermemory for similar past CI failures and fixes.
Fix the CI failures. Common fixes: Fix the CI failures. Common fixes:
- Biome lint errors: Run `bun run format-lint` or `biome check --fix .` - Biome lint errors: Run `bun run format-lint` or `biome check --fix .`
- Type errors: Run `bun run check-types` and fix reported issues - Type errors: Run `bun run check-types` and fix reported issues
@ -85,8 +87,21 @@ jobs:
After fixing, commit the changes and push directly to the branch `${{ github.event.workflow_run.head_branch }}`. After fixing, commit the changes and push directly to the branch `${{ github.event.workflow_run.head_branch }}`.
Do NOT create a new PR — the fixes should be pushed to the existing PR branch. Do NOT create a new PR — the fixes should be pushed to the existing PR branch.
Save the fix pattern to supermemory for future reference.
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: | claude_args: |
--max-turns 20 --max-turns 20
--model claude-opus-4-5-20251101 --model claude-opus-4-5-20251101
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__github" --allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__supermemory,mcp__github"
--mcp-config '{
"mcpServers": {
"supermemory": {
"type": "http",
"url": "https://mcp.supermemory.ai/mcp",
"headers": {
"Authorization": "Bearer ${{ secrets.SUPERMEMORY_API_KEY }}"
}
}
}
}'

View file

@ -38,7 +38,6 @@ jobs:
uses: anthropics/claude-code-action@v1 uses: anthropics/claude-code-action@v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
allowed_bots: "vorflux[bot]"
# Enable progress tracking # Enable progress tracking
track_progress: true track_progress: true
@ -49,7 +48,18 @@ jobs:
# Enable inline comments for specific issues # Enable inline comments for specific issues
claude_args: | claude_args: |
--model claude-opus-4-5-20251101 --model claude-opus-4-5-20251101
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__github__*" --allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__supermemory__*,mcp__github__*"
--mcp-config '{
"mcpServers": {
"supermemory": {
"type": "http",
"url": "https://mcp.supermemory.ai/mcp",
"headers": {
"Authorization": "Bearer ${{ secrets.SUPERMEMORY_API_KEY }}"
}
}
}
}'
prompt: | prompt: |
You are a senior engineer reviewing a pull request. Your job is to catch real bugs, security issues, and logic errors that a human reviewer might miss. You are NOT a linter — do not comment on style, naming, formatting, or minor nitpicks. You are a senior engineer reviewing a pull request. Your job is to catch real bugs, security issues, and logic errors that a human reviewer might miss. You are NOT a linter — do not comment on style, naming, formatting, or minor nitpicks.

View file

@ -67,4 +67,15 @@ jobs:
claude_args: | claude_args: |
--max-turns 15 --max-turns 15
--model claude-opus-4-5-20251101 --model claude-opus-4-5-20251101
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__github" --allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__supermemory,mcp__github"
--mcp-config '{
"mcpServers": {
"supermemory": {
"type": "http",
"url": "https://mcp.supermemory.ai/mcp",
"headers": {
"Authorization": "Bearer ${{ secrets.SUPERMEMORY_API_KEY }}"
}
}
}
}'

View file

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

View file

@ -38,65 +38,26 @@ jobs:
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
working-directory: . run: bun install
run: bun install --frozen-lockfile
- name: Check if version changed - name: Check if version changed
id: version-check id: version-check
run: | run: |
PACKAGE_NAME=$(jq -r '.name' package.json) PACKAGE_NAME=$(jq -r '.name' package.json)
LOCAL_VERSION=$(jq -r '.version' package.json) LOCAL_VERSION=$(jq -r '.version' package.json)
if npm view "$PACKAGE_NAME@$LOCAL_VERSION" version >/dev/null 2>&1; then NPM_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VERSION" = "$NPM_VERSION" ]; then
echo "Version $LOCAL_VERSION already published, skipping." echo "Version $LOCAL_VERSION already published, skipping."
echo "changed=false" >> "$GITHUB_OUTPUT" echo "changed=false" >> "$GITHUB_OUTPUT"
else else
echo "Publishing $LOCAL_VERSION." echo "Publishing $LOCAL_VERSION (npm has $NPM_VERSION)"
echo "changed=true" >> "$GITHUB_OUTPUT" echo "changed=true" >> "$GITHUB_OUTPUT"
fi fi
- name: Wait for the Tools dependency - name: Build
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' if: steps.version-check.outputs.changed == 'true'
run: bun run build 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 - name: Publish
if: steps.version-check.outputs.changed == 'true' if: steps.version-check.outputs.changed == 'true'
run: npm publish --access public --provenance run: npm publish --access public --provenance

View file

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

View file

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

View file

@ -7,7 +7,7 @@
</p> </p>
<p align="center"> <p align="center">
<strong>State-of-the-art memory and context engine for AI.</strong> <strong>State-of-the-art memory and context engine for AI. And yes - you can use it as a company/personal brain.</strong>
</p> </p>
<p align="center"> <p align="center">
@ -67,7 +67,7 @@ All of this is in our single memory structure and ontology.
<h3>🧑‍💻 I use AI tools</h3> <h3>🧑‍💻 I use AI tools</h3>
Give Claude Code, Cursor, Codex and OpenCode **persistent memory across every conversation** with a plugin or the MCP server. Build your own personal supermemory by using our app. Builds **persistent memory graph across every conversation**.
Your AI remembers your preferences, projects, past discussions — and gets smarter over time. Your AI remembers your preferences, projects, past discussions — and gets smarter over time.
@ -107,11 +107,21 @@ curl -fsSL https://supermemory.ai/install | bash
## Give your AI memory ## Give your AI memory
Plugins and the MCP server give any compatible AI assistant persistent memory. One install, and your AI remembers you. The Supermemory App, browser extension, plugins and MCP server gives any compatible AI assistant persistent memory. One install, and your AI remembers you.
### The app
You can use supermemory without any code, by using our consumer-facing app for free.
Start at https://app.supermemory.ai
<img width="1705" height="1030" alt="image" src="https://github.com/user-attachments/assets/5b43af30-b998-4585-8de6-f3e9a26d894a" />
It also comes with an agent embedded inside, which we call Nova.
### Supermemory Plugins ### Supermemory Plugins
Supermemory comes built with plugins for Claude Code, Cursor, Codex, OpenCode, OpenClaw, and Hermes. Supermemory comes built with Plugins for Claude Code, OpenCode, OpenClaw, and Hermes.
<img width="844" height="484" alt="image" src="https://github.com/user-attachments/assets/ecb879a2-8652-495d-9228-f305a97ba603" /> <img width="844" height="484" alt="image" src="https://github.com/user-attachments/assets/ecb879a2-8652-495d-9228-f305a97ba603" />
@ -119,30 +129,18 @@ These plugins are implementations of the supermemory API, and they are open sour
You can find them here: You can find them here:
- Claude Code plugin: https://github.com/supermemoryai/claude-supermemory - Openclaw plugin: https://github.com/supermemoryai/openclaw-supermemory
- Cursor plugin: https://github.com/supermemoryai/cursor-supermemory - Claude code plugin: https://github.com/supermemoryai/claude-supermemory
- Codex plugin: https://github.com/supermemoryai/codex-supermemory
- OpenClaw plugin: https://github.com/supermemoryai/openclaw-supermemory
- OpenCode plugin: https://github.com/supermemoryai/opencode-supermemory - OpenCode plugin: https://github.com/supermemoryai/opencode-supermemory
- Hermes agent (Supermemory memory provider): https://github.com/NousResearch/hermes-agent - Hermes agent (Supermemory memory provider): https://github.com/NousResearch/hermes-agent
### MCP ### MCP - Quick install
Server URL: ```bash
npx -y install-mcp@latest https://mcp.supermemory.ai/mcp --client claude --oauth=yes
```text
https://mcp.supermemory.ai/mcp
``` ```
```json Replace `claude` with your client: `cursor`, `windsurf`, `vscode`, etc.
{
"mcpServers": {
"supermemory": {
"url": "https://mcp.supermemory.ai/mcp"
}
}
}
```
Read more about our MCP here - https://supermemory.ai/docs/supermemory-mcp/mcp Read more about our MCP here - https://supermemory.ai/docs/supermemory-mcp/mcp
@ -184,6 +182,21 @@ Add this to your MCP client config:
} }
``` ```
Or use an API key instead of OAuth:
```json
{
"mcpServers": {
"supermemory": {
"url": "https://mcp.supermemory.ai/mcp",
"headers": {
"Authorization": "Bearer sm_your_api_key_here"
}
}
}
}
```
--- ---
## Build with Supermemory (API) ## Build with Supermemory (API)
@ -258,7 +271,7 @@ const agent = new Agent(withSupermemory(config, "user-123", { mode: "full" }));
```typescript ```typescript
// Hybrid (default) — RAG + Memory in one query // Hybrid (default) — RAG + Memory in one query
const results = await client.search({ const results = await client.search.memories({
q: "how do I deploy?", q: "how do I deploy?",
containerTag: "user_123", containerTag: "user_123",
searchMode: "hybrid", searchMode: "hybrid",
@ -266,7 +279,7 @@ const results = await client.search({
// Returns deployment docs (RAG) + user's deploy preferences (Memory) // Returns deployment docs (RAG) + user's deploy preferences (Memory)
// Memories only // Memories only
const results = await client.search({ const results = await client.search.memories({
q: "user preferences", q: "user preferences",
containerTag: "user_123", containerTag: "user_123",
searchMode: "memories", searchMode: "memories",
@ -300,8 +313,8 @@ Real-time webhooks. Documents automatically processed, chunked, and searchable.
|---|---| |---|---|
| `client.add()` | Store content — text, conversations, URLs, HTML | | `client.add()` | Store content — text, conversations, URLs, HTML |
| `client.profile()` | User profile + optional search in one call | | `client.profile()` | User profile + optional search in one call |
| `client.search()` | Hybrid search across memories and documents (`searchMode`) | | `client.search.memories()` | Hybrid search across memories and documents |
| `client.search.documents()` | Document search with metadata filters (legacy v3 response shape) | | `client.search.documents()` | Document search with metadata filters |
| `client.documents.uploadFile()` | Upload PDFs, images, videos, code | | `client.documents.uploadFile()` | Upload PDFs, images, videos, code |
| `client.documents.list()` | List and filter documents | | `client.documents.list()` | List and filter documents |
| `client.settings.update()` | Configure memory extraction and chunking | | `client.settings.update()` | Configure memory extraction and chunking |

View file

@ -7,7 +7,7 @@
</p> </p>
<p align="center"> <p align="center">
<strong>面向 AI 的记忆与上下文引擎,业界领先。</strong> <strong>面向 AI 的记忆与上下文引擎,业界领先。也可以把它当作公司或个人的「大脑」来用。</strong>
</p> </p>
<p align="center"> <p align="center">
@ -60,7 +60,7 @@ Supermemory 是为 AI 设计的记忆与上下文层。在 **[LongMemEval](https
<h3>🧑‍💻 我只是 AI 工具的用户</h3> <h3>🧑‍💻 我只是 AI 工具的用户</h3>
通过插件或 MCP 服务器,让 Claude Code、Cursor、Codex 和 OpenCode **在每次对话之间保持持久记忆** 直接用我们的应用,给自己搭一份专属的 supermemory。它会**在每次对话之间维护一张持久的记忆图谱**
你的 AI 会记住你的偏好、项目、历史讨论——而且越用越聪明。 你的 AI 会记住你的偏好、项目、历史讨论——而且越用越聪明。
@ -85,40 +85,38 @@ Supermemory 是为 AI 设计的记忆与上下文层。在 **[LongMemEval](https
## 给你的 AI 装上记忆 ## 给你的 AI 装上记忆
插件和 MCP 服务器可以为任何兼容的 AI 助手提供持久记忆。装一次AI 从此记住你。 Supermemory 的应用、浏览器扩展、插件和 MCP 服务器,可以为任何兼容的 AI 助手提供持久记忆。装一次AI 从此记住你。
### 应用
不用写代码,直接用我们面向消费者的应用——免费。
入口https://app.supermemory.ai
<img width="1705" height="1030" alt="image" src="https://github.com/user-attachments/assets/5b43af30-b998-4585-8de6-f3e9a26d894a" />
应用里内置了一个 agent我们叫它 Nova。
### Supermemory 插件 ### Supermemory 插件
Supermemory 已经为 Claude Code、Cursor、Codex、OpenCode、OpenClaw、Hermes 提供了开箱即用的插件。 Supermemory 已经为 Claude Code、OpenCode、OpenClaw、Hermes 提供了开箱即用的插件。
<img width="844" height="484" alt="image" src="https://github.com/user-attachments/assets/ecb879a2-8652-495d-9228-f305a97ba603" /> <img width="844" height="484" alt="image" src="https://github.com/user-attachments/assets/ecb879a2-8652-495d-9228-f305a97ba603" />
这些插件本质上是 supermemory API 的实现,全部开源: 这些插件本质上是 supermemory API 的实现,全部开源:
- Openclaw 插件https://github.com/supermemoryai/openclaw-supermemory
- Claude Code 插件https://github.com/supermemoryai/claude-supermemory - Claude Code 插件https://github.com/supermemoryai/claude-supermemory
- Cursor 插件https://github.com/supermemoryai/cursor-supermemory
- Codex 插件https://github.com/supermemoryai/codex-supermemory
- OpenClaw 插件https://github.com/supermemoryai/openclaw-supermemory
- OpenCode 插件https://github.com/supermemoryai/opencode-supermemory - OpenCode 插件https://github.com/supermemoryai/opencode-supermemory
- Hermes agentSupermemory 作为记忆 providerhttps://github.com/NousResearch/hermes-agent - Hermes agentSupermemory 作为记忆 providerhttps://github.com/NousResearch/hermes-agent
### MCP ### MCP——一键安装
服务地址: ```bash
npx -y install-mcp@latest https://mcp.supermemory.ai/mcp --client claude --oauth=yes
```text
https://mcp.supermemory.ai/mcp
``` ```
```json `claude` 换成你用的客户端即可:`cursor``windsurf``vscode` 等等。
{
"mcpServers": {
"supermemory": {
"url": "https://mcp.supermemory.ai/mcp"
}
}
}
```
更多 MCP 细节见https://supermemory.ai/docs/supermemory-mcp/mcp 更多 MCP 细节见https://supermemory.ai/docs/supermemory-mcp/mcp
@ -160,6 +158,21 @@ MCP 服务器开源——[查看源码](https://supermemory.ai/docs/supermemory-
} }
``` ```
如果想用 API key 代替 OAuth
```json
{
"mcpServers": {
"supermemory": {
"url": "https://mcp.supermemory.ai/mcp",
"headers": {
"Authorization": "Bearer sm_your_api_key_here"
}
}
}
}
```
--- ---
## 用 Supermemory API 构建 ## 用 Supermemory API 构建
@ -234,7 +247,7 @@ const agent = new Agent(withSupermemory(config, "user-123", { mode: "full" }));
```typescript ```typescript
// 混合检索(默认)——一次查询同时跑 RAG 和记忆 // 混合检索(默认)——一次查询同时跑 RAG 和记忆
const results = await client.search({ const results = await client.search.memories({
q: "how do I deploy?", q: "how do I deploy?",
containerTag: "user_123", containerTag: "user_123",
searchMode: "hybrid", searchMode: "hybrid",
@ -242,7 +255,7 @@ const results = await client.search({
// 返回部署文档RAG+ 该用户的部署偏好(记忆) // 返回部署文档RAG+ 该用户的部署偏好(记忆)
// 只查记忆 // 只查记忆
const results = await client.search({ const results = await client.search.memories({
q: "user preferences", q: "user preferences",
containerTag: "user_123", containerTag: "user_123",
searchMode: "memories", searchMode: "memories",
@ -276,8 +289,8 @@ const { profile } = await client.profile({ containerTag: "user_123" });
|---|---| |---|---|
| `client.add()` | 存储内容——文本、对话、URL、HTML | | `client.add()` | 存储内容——文本、对话、URL、HTML |
| `client.profile()` | 一次调用返回用户画像 + 可选检索 | | `client.profile()` | 一次调用返回用户画像 + 可选检索 |
| `client.search()` | 跨记忆和文档的混合检索`searchMode` | | `client.search.memories()` | 跨记忆和文档的混合检索 |
| `client.search.documents()` | 带元数据过滤的文档检索(旧版 v3 响应格式) | | `client.search.documents()` | 带元数据过滤的文档检索 |
| `client.documents.uploadFile()` | 上传 PDF、图片、视频、代码 | | `client.documents.uploadFile()` | 上传 PDF、图片、视频、代码 |
| `client.documents.list()` | 列出和筛选文档 | | `client.documents.list()` | 列出和筛选文档 |
| `client.settings.update()` | 配置记忆抽取与切分策略 | | `client.settings.update()` | 配置记忆抽取与切分策略 |

View file

@ -0,0 +1,2 @@
# PostHog Configuration
WXT_POSTHOG_API_KEY=your_posthog_project_api_key_here

26
apps/browser-extension/.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.output
stats.html
stats-*.json
.wxt
web-ext.config.ts
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1 @@
## supermemory Browser Extension

View file

@ -0,0 +1,18 @@
export function RightArrow({ className }: { className?: string }) {
return (
<svg
width="10"
height="11"
viewBox="0 0 10 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<title>Right arrow</title>
<path
d="M-1.26511e-05 5.82399V4.53599H7.81199L3.90599 0.895994L4.78799 -6.19888e-06L9.79999 4.77399V5.54399L4.78799 10.332L3.90599 9.43599L7.78399 5.82399H-1.26511e-05Z"
fill="#737373"
/>
</svg>
)
}

View file

@ -0,0 +1,288 @@
import {
getDefaultProject,
saveMemory,
searchMemories,
fetchProjects,
} from "../utils/api"
import {
CONTAINER_TAGS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
} from "../utils/constants"
import { trackEvent } from "../utils/posthog"
import { captureTwitterTokens } from "../utils/twitter-auth"
import {
type TwitterImportConfig,
TwitterImporter,
} from "../utils/twitter-import"
import type {
ExtensionMessage,
MemoryData,
MemoryPayload,
} from "../utils/types"
export default defineBackground(() => {
let twitterImporter: TwitterImporter | null = null
browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason === "install") {
await trackEvent("extension_installed", {
reason: details.reason,
version: browser.runtime.getManifest().version,
})
browser.tabs.create({
url: browser.runtime.getURL("/welcome.html"),
})
}
})
// Intercept Twitter requests to capture authentication headers.
browser.webRequest.onBeforeSendHeaders.addListener(
(details) => {
captureTwitterTokens(details)
return {}
},
{ urls: ["*://x.com/*", "*://twitter.com/*"] },
["requestHeaders", "extraHeaders"],
)
// Send message to current active tab.
const sendMessageToCurrentTab = async (message: string) => {
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
})
if (tabs.length > 0 && tabs[0].id) {
await browser.tabs.sendMessage(tabs[0].id, {
type: MESSAGE_TYPES.IMPORT_UPDATE,
importedMessage: message,
})
}
}
/**
* Send import completion message
*/
const sendImportDoneMessage = async (totalImported: number) => {
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
})
if (tabs.length > 0 && tabs[0].id) {
await browser.tabs.sendMessage(tabs[0].id, {
type: MESSAGE_TYPES.IMPORT_DONE,
totalImported,
})
}
}
/**
* Save memory to supermemory API
*/
const saveMemoryToSupermemory = async (
data: MemoryData,
actionSource: string,
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
try {
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT
try {
const defaultProject = await getDefaultProject()
if (defaultProject?.containerTag) {
containerTag = defaultProject.containerTag
}
} catch (error) {
console.warn("Failed to get default project, using fallback:", error)
}
let content: string
if (data.content) {
content = data.content
} else if (data.highlightedText) {
content = `${data.highlightedText}\n\n${data?.url || ""}`
} else if (data.markdown) {
content = `${data.markdown}\n\n${data?.url || ""}`
} else if (data.html) {
content = `${data.html}\n\n${data?.url || ""}`
} else {
content = data?.url || ""
}
const metadata: MemoryPayload["metadata"] = {
sm_source: "consumer",
website_url: data.url,
}
if (data.ogImage) {
metadata.website_og_image = data.ogImage
}
if (data.title) {
metadata.website_title = data.title
}
const payload: MemoryPayload = {
containerTags: [containerTag],
content,
metadata,
}
const responseData = await saveMemory(payload)
await trackEvent(POSTHOG_EVENT_KEY.SAVE_MEMORY_ATTEMPTED, {
source: `${POSTHOG_EVENT_KEY.SOURCE}_${actionSource}`,
has_highlight: !!data.highlightedText,
url_domain: data.url ? new URL(data.url).hostname : undefined,
})
return { success: true, data: responseData }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
}
}
const getRelatedMemories = async (
data: string,
eventSource: string,
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
try {
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT
try {
const defaultProject = await getDefaultProject()
if (defaultProject?.containerTag) {
containerTag = defaultProject.containerTag
}
} catch (error) {
console.warn("Failed to get default project, using fallback:", error)
}
const responseData = await searchMemories(data, containerTag)
const response = responseData as {
results?: Array<{ memory?: string }>
}
const memories: string[] = []
response.results?.forEach((result, index) => {
memories.push(`${index + 1}. ${result.memory} \n`)
})
console.log("Memories:", memories)
await trackEvent(eventSource)
return { success: true, data: memories }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
}
}
/**
* Handle extension messages
*/
browser.runtime.onMessage.addListener(
(message: ExtensionMessage, _sender, sendResponse) => {
// Handle Twitter import request
if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) {
const importConfig: TwitterImportConfig = {
isFolderImport: message.isFolderImport,
bookmarkCollectionId: message.bookmarkCollectionId,
selectedProject: message.selectedProject,
onProgress: sendMessageToCurrentTab,
onComplete: sendImportDoneMessage,
onError: async (error: Error) => {
await sendMessageToCurrentTab(`Error: ${error.message}`)
},
}
twitterImporter = new TwitterImporter(importConfig)
twitterImporter.startImport().catch(console.error)
sendResponse({ success: true })
return true
}
// Handle regular memory save request
if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
;(async () => {
try {
const result = await saveMemoryToSupermemory(
message.data as MemoryData,
message.actionSource || "unknown",
)
sendResponse(result)
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
})
}
})()
return true
}
if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) {
;(async () => {
try {
const result = await getRelatedMemories(
message.data as string,
message.actionSource || "unknown",
)
sendResponse(result)
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
})
}
})()
return true
}
if (message.action === MESSAGE_TYPES.CAPTURE_PROMPT) {
;(async () => {
try {
const messageData = message.data as {
prompt: string
platform: string
source: string
}
console.log("=== PROMPT CAPTURED ===")
console.log(messageData)
console.log("========================")
const memoryData: MemoryData = {
content: messageData.prompt,
}
const result = await saveMemoryToSupermemory(
memoryData,
`prompt_capture_${messageData.platform}`,
)
sendResponse(result)
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
})
}
})()
return true
}
if (message.action === MESSAGE_TYPES.FETCH_PROJECTS) {
;(async () => {
try {
const projects = await fetchProjects()
sendResponse({ success: true, data: projects })
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
})
}
})()
return true
}
},
)
})

View file

@ -0,0 +1,718 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
UI_CONFIG,
} from "../../utils/constants"
import {
autoSearchEnabled,
autoCapturePromptsEnabled,
} from "../../utils/storage"
import {
createChatGPTInputBarElement,
DOMUtils,
} from "../../utils/ui-components"
let chatGPTDebounceTimeout: NodeJS.Timeout | null = null
let chatGPTRouteObserver: MutationObserver | null = null
let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null
let chatGPTObserverThrottle: NodeJS.Timeout | null = null
export function initializeChatGPT() {
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
return
}
if (document.body.hasAttribute("data-chatgpt-initialized")) {
return
}
setTimeout(() => {
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
setupChatGPTAutoFetch()
}, 2000)
setupChatGPTPromptCapture()
setupChatGPTRouteChangeDetection()
document.body.setAttribute("data-chatgpt-initialized", "true")
}
function setupChatGPTRouteChangeDetection() {
if (chatGPTRouteObserver) {
chatGPTRouteObserver.disconnect()
}
if (chatGPTUrlCheckInterval) {
clearInterval(chatGPTUrlCheckInterval)
}
if (chatGPTObserverThrottle) {
clearTimeout(chatGPTObserverThrottle)
chatGPTObserverThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log("ChatGPT route changed, re-adding supermemory elements")
setTimeout(() => {
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
setupChatGPTAutoFetch()
}, 1000)
}
}
chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 2000)
chatGPTRouteObserver = new MutationObserver((mutations) => {
if (chatGPTObserverThrottle) {
return
}
let shouldRecheck = false
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (
element.querySelector?.("#prompt-textarea") ||
element.querySelector?.("button.composer-btn") ||
element.querySelector?.('[role="dialog"]') ||
element.matches?.("#prompt-textarea") ||
element.id === "prompt-textarea"
) {
shouldRecheck = true
}
}
})
}
})
if (shouldRecheck) {
chatGPTObserverThrottle = setTimeout(() => {
try {
chatGPTObserverThrottle = null
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
setupChatGPTAutoFetch()
} catch (error) {
console.error("Error in ChatGPT observer callback:", error)
}
}, 300)
}
})
try {
chatGPTRouteObserver.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error("Failed to set up ChatGPT route observer:", error)
if (chatGPTUrlCheckInterval) {
clearInterval(chatGPTUrlCheckInterval)
}
chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
async function getRelatedMemoriesForChatGPT(actionSource: string) {
try {
const userQuery =
document.getElementById("prompt-textarea")?.textContent || ""
const icon = document.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)[0]
const iconElement = icon as HTMLElement
if (!iconElement) {
console.warn("ChatGPT icon element not found, cannot update feedback")
return
}
updateChatGPTIconFeedback("Searching memories...", iconElement)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Memory search timeout")),
UI_CONFIG.API_REQUEST_TIMEOUT,
),
)
const response = await Promise.race([
browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: actionSource,
}),
timeoutPromise,
])
if (response?.success && response?.data) {
const promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
promptElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
console.log(
"Prompt element dataset:",
promptElement.dataset.supermemories,
)
iconElement.dataset.memoriesData = response.data
updateChatGPTIconFeedback("Included Memories", iconElement)
} else {
console.warn(
"ChatGPT prompt element not found after successful memory fetch",
)
updateChatGPTIconFeedback("Memories found", iconElement)
}
} else {
console.warn("No memories found or API response invalid")
updateChatGPTIconFeedback("No memories found", iconElement)
}
} catch (error) {
console.error("Error getting related memories:", error)
try {
const icon = document.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)[0] as HTMLElement
if (icon) {
updateChatGPTIconFeedback("Error fetching memories", icon)
}
} catch (feedbackError) {
console.error("Failed to update error feedback:", feedbackError)
}
}
}
function addSupermemoryButtonToMemoriesDialog() {
const dialogs = document.querySelectorAll('[role="dialog"]')
let memoriesDialog: HTMLElement | null = null
for (const dialog of dialogs) {
const headerText = dialog.querySelector("h2")
if (headerText?.textContent?.includes("Saved memories")) {
memoriesDialog = dialog as HTMLElement
break
}
}
if (!memoriesDialog) return
if (memoriesDialog.querySelector("#supermemory-save-button")) return
const deleteAllContainer = memoriesDialog.querySelector(
".flex.items-center.gap-0\\.5",
)
if (!deleteAllContainer) return
const supermemoryButton = document.createElement("button")
supermemoryButton.id = "supermemory-save-button"
supermemoryButton.className = "btn relative btn-primary-outline mr-2"
const iconUrl = browser.runtime.getURL("/icon-16.png")
supermemoryButton.innerHTML = `
<div class="flex items-center justify-center gap-2">
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
Save to supermemory
</div>
`
supermemoryButton.style.cssText = `
background: #1C2026 !important;
color: white !important;
border: 1px solid #1C2026 !important;
border-radius: 9999px !important;
padding: 10px 16px !important;
font-weight: 500 !important;
font-size: 14px !important;
margin-right: 8px !important;
cursor: pointer !important;
`
supermemoryButton.addEventListener("mouseenter", () => {
supermemoryButton.style.backgroundColor = "#2B2E33"
})
supermemoryButton.addEventListener("mouseleave", () => {
supermemoryButton.style.backgroundColor = "#1C2026"
})
supermemoryButton.addEventListener("click", async () => {
await saveMemoriesToSupermemory()
})
deleteAllContainer.insertBefore(
supermemoryButton,
deleteAllContainer.firstChild,
)
}
async function saveMemoriesToSupermemory() {
try {
DOMUtils.showToast("loading")
const memoriesTable = document.querySelector('[role="dialog"] table tbody')
if (!memoriesTable) {
DOMUtils.showToast("error")
return
}
if (!memoriesTable.textContent) {
DOMUtils.showToast("error")
return
}
const combinedContent = `Memories from ChatGPT:\n\n${memoriesTable.textContent}`
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
html: combinedContent,
},
actionSource: "chatgpt_memories_dialog",
})
console.log({ response })
if (response.success) {
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving memories to supermemory:", error)
DOMUtils.showToast("error")
}
}
function updateChatGPTIconFeedback(
message: string,
iconElement: HTMLElement,
resetAfter = 0,
) {
if (!iconElement.dataset.originalHtml) {
iconElement.dataset.originalHtml = iconElement.innerHTML
}
const feedbackDiv = document.createElement("div")
feedbackDiv.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: #513EA9;
border-radius: 12px;
color: white;
font-size: 12px;
font-weight: 500;
cursor: ${message === "Included Memories" ? "pointer" : "default"};
position: relative;
`
feedbackDiv.innerHTML = `
<span></span>
<span>${message}</span>
`
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
const popup = document.createElement("div")
popup.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
padding: 0;
border-radius: 12px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
max-width: 500px;
max-height: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 999999;
display: none;
border: 1px solid #333;
`
const header = document.createElement("div")
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
`
header.innerHTML = `
<span style="font-weight: 600; color: #fff;">Included Memories</span>
`
const content = document.createElement("div")
content.style.cssText = `
padding: 0;
max-height: 300px;
overflow-y: auto;
`
const memoriesText = iconElement.dataset.memoriesData || ""
console.log("Memories text:", memoriesText)
const individualMemories = memoriesText
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
console.log("Individual memories:", individualMemories)
individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div")
memoryItem.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 10px;
font-size: 13px;
line-height: 1.4;
`
const memoryText = document.createElement("div")
memoryText.style.cssText = `
flex: 1;
color: #e5e5e5;
`
memoryText.textContent = memory.trim()
const removeBtn = document.createElement("button")
removeBtn.style.cssText = `
background: transparent;
color: #9ca3af;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
`
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
removeBtn.dataset.memoryIndex = index.toString()
removeBtn.addEventListener("mouseenter", () => {
removeBtn.style.color = "#ef4444"
})
removeBtn.addEventListener("mouseleave", () => {
removeBtn.style.color = "#9ca3af"
})
memoryItem.appendChild(memoryText)
memoryItem.appendChild(removeBtn)
content.appendChild(memoryItem)
})
popup.appendChild(header)
popup.appendChild(content)
document.body.appendChild(popup)
feedbackDiv.addEventListener("mouseenter", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Click to see memories"
}
})
feedbackDiv.addEventListener("mouseleave", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Included Memories"
}
})
feedbackDiv.addEventListener("click", (e) => {
e.stopPropagation()
popup.style.display = "block"
})
document.addEventListener("click", (e) => {
if (!popup.contains(e.target as Node)) {
popup.style.display = "none"
}
})
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
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 promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
promptElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}`
}
content
.querySelectorAll("button[data-memory-index]")
.forEach((btn, newIndex) => {
const htmlBtn = btn as HTMLButtonElement
htmlBtn.dataset.memoryIndex = newIndex.toString()
})
if (currentMemories.length <= 1) {
if (promptElement?.dataset.supermemories) {
delete promptElement.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 = ""
iconElement.appendChild(feedbackDiv)
if (resetAfter > 0) {
setTimeout(() => {
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}, resetAfter)
}
}
function addSaveChatGPTElementBeforeComposerBtn() {
const composerButtons = document.querySelectorAll("button.composer-btn")
composerButtons.forEach((button) => {
if (button.hasAttribute("data-supermemory-icon-added-before")) {
return
}
const parent = button.parentElement
if (!parent) return
const parentSiblings = parent.parentElement?.children
if (!parentSiblings) return
let hasSpeechButtonSibling = false
for (const sibling of parentSiblings) {
if (
sibling.getAttribute("data-testid") ===
"composer-speech-button-container"
) {
hasSpeechButtonSibling = true
break
}
}
if (!hasSpeechButtonSibling) return
const grandParent = parent.parentElement
if (!grandParent) return
const existingIcon = grandParent.querySelector(
`#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
)
if (existingIcon) {
button.setAttribute("data-supermemory-icon-added-before", "true")
return
}
const saveChatGPTElement = createChatGPTInputBarElement(async () => {
await getRelatedMemoriesForChatGPT(
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
)
})
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
button.setAttribute("data-supermemory-icon-added-before", "true")
grandParent.insertBefore(saveChatGPTElement, parent)
setupChatGPTAutoFetch()
})
}
async function setupChatGPTAutoFetch() {
const autoSearch = (await autoSearchEnabled.getValue()) ?? false
if (!autoSearch) {
return
}
const promptTextarea = document.getElementById("prompt-textarea")
if (
!promptTextarea ||
promptTextarea.hasAttribute("data-supermemory-auto-fetch")
) {
return
}
promptTextarea.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => {
if (chatGPTDebounceTimeout) {
clearTimeout(chatGPTDebounceTimeout)
}
chatGPTDebounceTimeout = setTimeout(async () => {
const content = promptTextarea.textContent?.trim() || ""
if (content.length > 2) {
await getRelatedMemoriesForChatGPT(
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED,
)
} else if (content.length === 0) {
const icons = document.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (promptTextarea.dataset.supermemories) {
delete promptTextarea.dataset.supermemories
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
}
promptTextarea.addEventListener("input", handleInput)
}
function setupChatGPTPromptCapture() {
if (document.body.hasAttribute("data-chatgpt-prompt-capture-setup")) {
return
}
document.body.setAttribute("data-chatgpt-prompt-capture-setup", "true")
const capturePromptContent = async (source: string) => {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) {
console.log("Auto capture prompts is disabled, skipping prompt capture")
return
}
const promptTextarea = document.getElementById("prompt-textarea")
let promptContent = ""
if (promptTextarea) {
promptContent = promptTextarea.textContent || ""
}
const storedMemories = promptTextarea?.dataset.supermemories
if (
storedMemories &&
promptTextarea &&
!promptContent.includes("Supermemories of user")
) {
promptTextarea.appendChild(document.createTextNode(storedMemories))
promptContent = promptTextarea.textContent || ""
}
if (promptTextarea && promptContent.trim()) {
console.log(`ChatGPT prompt submitted via ${source}:`, promptContent)
try {
await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "chatgpt",
source: source,
},
})
} catch (error) {
console.error("Error sending ChatGPT prompt to background:", error)
}
}
const icons = document.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (promptTextarea?.dataset.supermemories) {
delete promptTextarea.dataset.supermemories
}
}
document.addEventListener(
"click",
async (event) => {
const target = event.target as HTMLElement
if (
target.id === "composer-submit-button" ||
target.closest("#composer-submit-button")
) {
await capturePromptContent("button click")
}
},
true,
)
document.addEventListener(
"keydown",
async (event) => {
const target = event.target as HTMLElement
if (
target.id === "prompt-textarea" &&
event.key === "Enter" &&
!event.shiftKey
) {
await capturePromptContent("Enter key")
}
},
true,
)
}

View file

@ -0,0 +1,844 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
UI_CONFIG,
} from "../../utils/constants"
import {
autoSearchEnabled,
autoCapturePromptsEnabled,
} from "../../utils/storage"
import {
createClaudeInputBarElement,
DOMUtils,
} from "../../utils/ui-components"
let claudeDebounceTimeout: NodeJS.Timeout | null = null
let claudeRouteObserver: MutationObserver | null = null
let claudeUrlCheckInterval: NodeJS.Timeout | null = null
let claudeObserverThrottle: NodeJS.Timeout | null = null
export function initializeClaude() {
if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
return
}
if (document.body.hasAttribute("data-claude-initialized")) {
return
}
setTimeout(() => {
addSupermemoryButtonToClaudeMemoryDialog()
addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch()
}, 2000)
setupClaudePromptCapture()
setupClaudeRouteChangeDetection()
document.body.setAttribute("data-claude-initialized", "true")
}
function setupClaudeRouteChangeDetection() {
if (claudeRouteObserver) {
claudeRouteObserver.disconnect()
}
if (claudeUrlCheckInterval) {
clearInterval(claudeUrlCheckInterval)
}
if (claudeObserverThrottle) {
clearTimeout(claudeObserverThrottle)
claudeObserverThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log("Claude route changed, re-adding supermemory icon")
setTimeout(() => {
addSupermemoryButtonToClaudeMemoryDialog()
addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch()
}, 1000)
}
}
claudeUrlCheckInterval = setInterval(checkForRouteChange, 2000)
claudeRouteObserver = new MutationObserver((mutations) => {
if (claudeObserverThrottle) {
return
}
let shouldRecheck = false
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (
element.querySelector?.('[role="dialog"]') ||
element.querySelector?.('div[contenteditable="true"]') ||
element.querySelector?.("textarea") ||
element.matches?.('[role="dialog"]') ||
element.matches?.('div[contenteditable="true"]') ||
element.matches?.("textarea") ||
element.textContent?.includes("Manage memory")
) {
shouldRecheck = true
}
}
})
}
})
if (shouldRecheck) {
claudeObserverThrottle = setTimeout(() => {
try {
claudeObserverThrottle = null
addSupermemoryButtonToClaudeMemoryDialog()
addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch()
} catch (error) {
console.error("Error in Claude observer callback:", error)
}
}, 300)
}
})
try {
claudeRouteObserver.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error("Failed to set up Claude route observer:", error)
if (claudeUrlCheckInterval) {
clearInterval(claudeUrlCheckInterval)
}
claudeUrlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
function addSupermemoryIconToClaudeInput() {
const targetContainers = document.querySelectorAll(
".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0",
)
targetContainers.forEach((container) => {
if (container.hasAttribute("data-supermemory-icon-added")) {
return
}
const existingIcon = container.querySelector(
`#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`,
)
if (existingIcon) {
container.setAttribute("data-supermemory-icon-added", "true")
return
}
const supermemoryIcon = createClaudeInputBarElement(async () => {
await getRelatedMemoriesForClaude(
POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED,
)
})
supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true")
container.insertBefore(supermemoryIcon, container.firstChild)
})
}
async function getRelatedMemoriesForClaude(actionSource: string) {
try {
let userQuery = ""
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (supermemoryContainer?.parentElement?.previousElementSibling) {
const pTag =
supermemoryContainer.parentElement.previousElementSibling.querySelector(
"p",
)
userQuery = pTag?.innerText || pTag?.textContent || ""
}
if (!userQuery.trim()) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
userQuery =
textareaElement?.innerText || textareaElement?.textContent || ""
}
if (!userQuery.trim()) {
const inputElements = document.querySelectorAll(
'div[contenteditable="true"], textarea, input[type="text"]',
)
for (const element of inputElements) {
const text =
(element as HTMLElement).innerText ||
(element as HTMLInputElement).value
if (text?.trim()) {
userQuery = text.trim()
break
}
}
}
console.log("Claude query extracted:", userQuery)
if (!userQuery.trim()) {
console.log("No query text found for Claude")
return
}
const icon = document.querySelector('[id*="sm-claude-input-bar-element"]')
const iconElement = icon as HTMLElement
if (!iconElement) {
console.warn("Claude icon element not found, cannot update feedback")
return
}
updateClaudeIconFeedback("Searching memories...", iconElement)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Memory search timeout")),
UI_CONFIG.API_REQUEST_TIMEOUT,
),
)
const response = await Promise.race([
browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: actionSource,
}),
timeoutPromise,
])
console.log("Claude memories response:", response)
if (response?.success && response?.data) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
console.log(
"Text element dataset:",
textareaElement.dataset.supermemories,
)
iconElement.dataset.memoriesData = response.data
updateClaudeIconFeedback("Included Memories", iconElement)
} else {
console.warn(
"Claude input area not found after successful memory fetch",
)
updateClaudeIconFeedback("Memories found", iconElement)
}
} else {
console.warn("No memories found or API response invalid for Claude")
updateClaudeIconFeedback("No memories found", iconElement)
}
} catch (error) {
console.error("Error getting related memories for Claude:", error)
try {
const icon = document.querySelector(
'[id*="sm-claude-input-bar-element"]',
) as HTMLElement
if (icon) {
updateClaudeIconFeedback("Error fetching memories", icon)
}
} catch (feedbackError) {
console.error("Failed to update Claude error feedback:", feedbackError)
}
}
}
function getClaudeMemoryDialog(): HTMLElement | null {
const dialogs = Array.from(
document.querySelectorAll<HTMLElement>('[role="dialog"]'),
)
for (const dialog of dialogs) {
const heading = Array.from(dialog.querySelectorAll("h1, h2, h3")).find(
(element) => element.textContent?.trim() === "Manage memory",
)
if (heading) return dialog
}
const candidates = Array.from(document.querySelectorAll<HTMLElement>("div"))
.filter((element) => {
const text = element.textContent || ""
if (
!text.includes("Manage memory") ||
!text.includes("Here's what Claude remembers")
) {
return false
}
const rect = element.getBoundingClientRect()
return rect.width > 400 && rect.height > 250
})
.sort((a, b) => {
const rectA = a.getBoundingClientRect()
const rectB = b.getBoundingClientRect()
return rectA.width * rectA.height - rectB.width * rectB.height
})
return candidates[0] || null
}
function getClaudeMemoryText(dialog: HTMLElement): string {
const clonedDialog = dialog.cloneNode(true) as HTMLElement
clonedDialog.querySelector("#supermemory-save-button")?.remove()
const sanitizeClaudeMemoryText = (text: string) =>
text
.replace(/^Memories from Claude:\s*/i, "")
.split("\n")
.map((line) => line.trim())
.filter(
(line) =>
line &&
line !== "Tell Claude what to remember or forget..." &&
line !== "Save to supermemory",
)
.join("\n")
.trim()
const memorySections = Array.from(
clonedDialog.querySelectorAll<HTMLElement>(
"article, section, [class*='border'], [class*='rounded']",
),
)
.map((element) => element.innerText || element.textContent || "")
.map(sanitizeClaudeMemoryText)
.filter((text) => {
return (
text.length > 80 &&
!text.includes("Manage edits") &&
!text.includes("Save to supermemory") &&
!text.includes("Tell Claude what to remember or forget")
)
})
.sort((a, b) => b.length - a.length)
if (memorySections[0]) return memorySections[0]
return sanitizeClaudeMemoryText(
clonedDialog.innerText || clonedDialog.textContent || "",
)
}
function addSupermemoryButtonToClaudeMemoryDialog() {
const memoryDialog = getClaudeMemoryDialog()
if (!memoryDialog) return
if (memoryDialog.querySelector("#supermemory-save-button")) return
const supermemoryButton = document.createElement("button")
supermemoryButton.id = "supermemory-save-button"
const iconUrl = browser.runtime.getURL("/icon-16.png")
supermemoryButton.innerHTML = `
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 8px; white-space: nowrap;">
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
<span style="white-space: nowrap;">Save to supermemory</span>
</div>
`
supermemoryButton.style.cssText = `
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: auto !important;
min-width: 190px !important;
background: #1C2026 !important;
color: white !important;
border: 1px solid #1C2026 !important;
border-radius: 9999px !important;
padding: 10px 16px !important;
font-weight: 500 !important;
font-size: 14px !important;
line-height: 20px !important;
white-space: nowrap !important;
margin: 8px 0 8px 0 !important;
transform: translateX(-16px) !important;
cursor: pointer !important;
font-family: inherit !important;
`
supermemoryButton.addEventListener("mouseenter", () => {
supermemoryButton.style.backgroundColor = "#2B2E33"
})
supermemoryButton.addEventListener("mouseleave", () => {
supermemoryButton.style.backgroundColor = "#1C2026"
})
supermemoryButton.addEventListener("click", async () => {
await saveClaudeMemoriesToSupermemory(memoryDialog)
})
const introText = Array.from(
memoryDialog.querySelectorAll<HTMLElement>("p, div"),
).find((element) =>
element.textContent?.includes("Here's what Claude remembers"),
)
if (introText?.parentElement) {
introText.parentElement.insertBefore(
supermemoryButton,
introText.nextSibling,
)
return
}
const heading = Array.from(memoryDialog.querySelectorAll("h1, h2, h3")).find(
(element) => element.textContent?.trim() === "Manage memory",
)
if (heading?.parentElement) {
heading.parentElement.insertBefore(supermemoryButton, heading.nextSibling)
return
}
memoryDialog.insertBefore(supermemoryButton, memoryDialog.firstChild)
}
async function saveClaudeMemoriesToSupermemory(memoryDialog: HTMLElement) {
try {
DOMUtils.showToast("loading")
const memoryText = getClaudeMemoryText(memoryDialog)
if (!memoryText) {
DOMUtils.showToast("error")
return
}
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
html: memoryText,
},
actionSource: "claude_memories_dialog",
})
console.log({ response })
if (response.success) {
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving Claude memories to supermemory:", error)
DOMUtils.showToast("error")
}
}
function updateClaudeIconFeedback(
message: string,
iconElement: HTMLElement,
resetAfter = 0,
) {
if (!iconElement.dataset.originalHtml) {
iconElement.dataset.originalHtml = iconElement.innerHTML
}
const feedbackDiv = document.createElement("div")
feedbackDiv.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: #513EA9;
border-radius: 6px;
color: white;
font-size: 12px;
font-weight: 500;
cursor: ${message === "Included Memories" ? "pointer" : "default"};
position: relative;
`
feedbackDiv.innerHTML = `
<span></span>
<span>${message}</span>
`
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
const popup = document.createElement("div")
popup.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
padding: 0;
border-radius: 12px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
max-width: 500px;
max-height: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 999999;
display: none;
border: 1px solid #333;
`
const header = document.createElement("div")
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
`
header.innerHTML = `
<span style="font-weight: 600; color: #fff;">Included Memories</span>
`
const content = document.createElement("div")
content.style.cssText = `
padding: 0;
max-height: 300px;
overflow-y: auto;
`
const memoriesText = iconElement.dataset.memoriesData || ""
console.log("Memories text:", memoriesText)
const individualMemories = memoriesText
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
console.log("Individual memories:", individualMemories)
individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div")
memoryItem.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 10px;
font-size: 13px;
line-height: 1.4;
`
const memoryText = document.createElement("div")
memoryText.style.cssText = `
flex: 1;
color: #e5e5e5;
`
memoryText.textContent = memory.trim()
const removeBtn = document.createElement("button")
removeBtn.style.cssText = `
background: transparent;
color: #9ca3af;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
`
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
removeBtn.dataset.memoryIndex = index.toString()
removeBtn.addEventListener("mouseenter", () => {
removeBtn.style.color = "#ef4444"
})
removeBtn.addEventListener("mouseleave", () => {
removeBtn.style.color = "#9ca3af"
})
memoryItem.appendChild(memoryText)
memoryItem.appendChild(removeBtn)
content.appendChild(memoryItem)
})
popup.appendChild(header)
popup.appendChild(content)
document.body.appendChild(popup)
feedbackDiv.addEventListener("mouseenter", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Click to see memories"
}
})
feedbackDiv.addEventListener("mouseleave", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Included Memories"
}
})
feedbackDiv.addEventListener("click", (e) => {
e.stopPropagation()
popup.style.display = "block"
})
document.addEventListener("click", (e) => {
if (!popup.contains(e.target as Node)) {
popup.style.display = "none"
}
})
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
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 textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}`
}
content
.querySelectorAll("button[data-memory-index]")
.forEach((btn, newIndex) => {
const htmlBtn = btn as HTMLButtonElement
htmlBtn.dataset.memoryIndex = newIndex.toString()
})
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 = ""
iconElement.appendChild(feedbackDiv)
if (resetAfter > 0) {
setTimeout(() => {
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}, resetAfter)
}
}
function setupClaudePromptCapture() {
if (document.body.hasAttribute("data-claude-prompt-capture-setup")) {
return
}
document.body.setAttribute("data-claude-prompt-capture-setup", "true")
const captureClaudePromptContent = async (source: string) => {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) {
console.log("Auto capture prompts is disabled, skipping prompt capture")
return
}
let promptContent = ""
const contentEditableDiv = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (contentEditableDiv) {
promptContent =
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
}
if (!promptContent) {
const textarea = document.querySelector("textarea") as HTMLTextAreaElement
if (textarea) {
promptContent = textarea.value || ""
}
}
const storedMemories = contentEditableDiv?.dataset.supermemories
if (
storedMemories &&
contentEditableDiv &&
!promptContent.includes("Supermemories of user")
) {
contentEditableDiv.appendChild(document.createTextNode(storedMemories))
promptContent =
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
}
if (promptContent.trim()) {
console.log(`Claude prompt submitted via ${source}:`, promptContent)
try {
await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "claude",
source: source,
},
})
} catch (error) {
console.error("Error sending Claude prompt to background:", error)
}
}
const icons = document.querySelectorAll(
'[id*="sm-claude-input-bar-element"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (contentEditableDiv?.dataset.supermemories) {
delete contentEditableDiv.dataset.supermemories
}
}
document.addEventListener(
"click",
async (event) => {
const target = event.target as HTMLElement
const sendButton =
target.closest(
"button.inline-flex.items-center.justify-center.relative.shrink-0.can-focus.select-none",
) ||
target.closest('button[class*="bg-accent-main-000"]') ||
target.closest('button[class*="rounded-lg"]')
if (sendButton) {
await captureClaudePromptContent("button click")
}
},
true,
)
document.addEventListener(
"keydown",
async (event) => {
const target = event.target as HTMLElement
if (
(target.matches('div[contenteditable="true"]') ||
target.matches(".ProseMirror") ||
target.matches("textarea") ||
target.closest(".ProseMirror")) &&
event.key === "Enter" &&
!event.shiftKey
) {
await captureClaudePromptContent("Enter key")
}
},
true,
)
}
async function setupClaudeAutoFetch() {
const autoSearch = (await autoSearchEnabled.getValue()) ?? false
if (!autoSearch) {
return
}
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (
!textareaElement ||
textareaElement.hasAttribute("data-supermemory-auto-fetch")
) {
return
}
textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => {
if (claudeDebounceTimeout) {
clearTimeout(claudeDebounceTimeout)
}
claudeDebounceTimeout = setTimeout(async () => {
const content = textareaElement.textContent?.trim() || ""
if (content.length > 2) {
await getRelatedMemoriesForClaude(
POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED,
)
} else if (content.length === 0) {
const icons = document.querySelectorAll(
'[id*="sm-claude-input-bar-element"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (textareaElement.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
}
textareaElement.addEventListener("input", handleInput)
}

View file

@ -0,0 +1,445 @@
import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
import { DOMUtils } from "../../utils/ui-components"
let grokRouteObserver: MutationObserver | null = null
let grokUrlCheckInterval: NodeJS.Timeout | null = null
let grokObserverThrottle: NodeJS.Timeout | null = null
const GROK_IMPORT_INTENT_PARAM = "sm_grok_import"
const GROK_IMPORT_INTENT_VALUE = "memories"
export function initializeGrok() {
if (!DOMUtils.isOnDomain(DOMAINS.GROK)) {
return
}
if (document.body.hasAttribute("data-grok-initialized")) {
return
}
setTimeout(() => {
addSupermemoryButtonToGrokMemoryDialog()
handleGrokImportIntent()
}, 1000)
setupGrokRouteChangeDetection()
document.body.setAttribute("data-grok-initialized", "true")
}
function setupGrokRouteChangeDetection() {
if (grokRouteObserver) {
grokRouteObserver.disconnect()
}
if (grokUrlCheckInterval) {
clearInterval(grokUrlCheckInterval)
}
if (grokObserverThrottle) {
clearTimeout(grokObserverThrottle)
grokObserverThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
setTimeout(() => {
addSupermemoryButtonToGrokMemoryDialog()
handleGrokImportIntent()
}, 500)
}
}
grokUrlCheckInterval = setInterval(checkForRouteChange, 2000)
grokRouteObserver = new MutationObserver((mutations) => {
if (grokObserverThrottle) {
return
}
let shouldRecheck = false
for (const mutation of mutations) {
if (mutation.type !== "childList" || mutation.addedNodes.length === 0) {
continue
}
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) {
continue
}
const element = node as Element
const text = element.textContent || ""
if (
element.querySelector?.('[role="dialog"]') ||
element.matches?.('[role="dialog"]') ||
text.includes("Data Controls") ||
text.includes("Settings") ||
text.includes("Memory from your chats")
) {
shouldRecheck = true
break
}
}
}
if (shouldRecheck) {
grokObserverThrottle = setTimeout(() => {
grokObserverThrottle = null
addSupermemoryButtonToGrokMemoryDialog()
handleGrokImportIntent()
}, 250)
}
})
try {
grokRouteObserver.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error("Failed to set up Grok route observer:", error)
if (grokUrlCheckInterval) {
clearInterval(grokUrlCheckInterval)
}
grokUrlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
function hasGrokImportIntent() {
return (
new URLSearchParams(window.location.search).get(
GROK_IMPORT_INTENT_PARAM,
) === GROK_IMPORT_INTENT_VALUE
)
}
function clearGrokImportIntent() {
const url = new URL(window.location.href)
url.searchParams.delete(GROK_IMPORT_INTENT_PARAM)
window.history.replaceState(window.history.state, "", url.toString())
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function isVisible(element: HTMLElement) {
const rect = element.getBoundingClientRect()
const style = window.getComputedStyle(element)
return (
rect.width > 0 &&
rect.height > 0 &&
style.display !== "none" &&
style.visibility !== "hidden" &&
Number.parseFloat(style.opacity || "1") > 0
)
}
function getNormalizedText(element: Element) {
return (element.textContent || "").replace(/\s+/g, " ").trim()
}
function clickVisibleElementByText(
labels: string[],
root: ParentNode = document,
) {
const elements = Array.from(
root.querySelectorAll<HTMLElement>(
"button, a, [role='button'], [role='tab'], [data-testid], div, span",
),
)
for (const label of labels) {
const matchingElement = elements.find((element) => {
const text = getNormalizedText(element)
return text === label && isVisible(element)
})
if (!matchingElement) {
continue
}
const clickableElement =
matchingElement.closest<HTMLElement>(
"button, a, [role='button'], [role='tab']",
) || matchingElement
clickableElement.click()
return true
}
return false
}
function getGrokSettingsDialog() {
return Array.from(
document.querySelectorAll<HTMLElement>('[role="dialog"]'),
).find((dialog) => {
const text = getNormalizedText(dialog)
return (
isVisible(dialog) &&
text.includes("Data Controls") &&
text.includes("Appearance") &&
text.includes("Behavior")
)
})
}
function isGrokDataControlsVisible() {
const text = getNormalizedText(document.body)
return (
text.includes("Data Controls") && text.includes("Memory from your chats")
)
}
async function handleGrokImportIntent() {
if (!hasGrokImportIntent()) return
if (document.body.hasAttribute("data-grok-import-intent-running")) {
return
}
document.body.setAttribute("data-grok-import-intent-running", "true")
for (let attempt = 0; attempt < 24; attempt++) {
addSupermemoryButtonToGrokMemoryDialog()
if (getGrokMemoryDialog()) {
clearGrokImportIntent()
document.body.removeAttribute("data-grok-import-intent-running")
return
}
const settingsDialog = getGrokSettingsDialog()
if (settingsDialog) {
if (isGrokDataControlsVisible()) {
clearGrokImportIntent()
document.body.removeAttribute("data-grok-import-intent-running")
return
}
clickVisibleElementByText(["Data Controls"], settingsDialog)
} else {
clickVisibleElementByText(["Settings"], document)
}
await sleep(350)
}
document.body.removeAttribute("data-grok-import-intent-running")
}
function getGrokMemoryDialog(): HTMLElement | null {
const dialogs = Array.from(
document.querySelectorAll<HTMLElement>('[role="dialog"]'),
)
for (const dialog of dialogs) {
const heading = Array.from(dialog.querySelectorAll("h1, h2, h3")).find(
(element) => element.textContent?.trim() === "Memory from your chats",
)
if (heading) return dialog
}
const candidates = Array.from(document.querySelectorAll<HTMLElement>("div"))
.filter((element) => {
const text = element.textContent || ""
if (
!text.includes("Memory from your chats") ||
!text.includes("This summary is regenerated")
) {
return false
}
const rect = element.getBoundingClientRect()
return rect.width > 400 && rect.height > 250
})
.sort((a, b) => {
const rectA = a.getBoundingClientRect()
const rectB = b.getBoundingClientRect()
return rectA.width * rectA.height - rectB.width * rectB.height
})
return candidates[0] || null
}
const GROK_MEMORY_UI_TEXT = [
"Memory from your chats",
"This summary is regenerated periodically from your conversations.",
"Save to supermemory",
"Close",
"Delete memory",
"Edit",
] as const
function escapeRegExp(text: string) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
function sanitizeGrokMemoryText(text: string) {
let sanitizedText = text
for (const uiText of GROK_MEMORY_UI_TEXT) {
sanitizedText = sanitizedText.replace(
new RegExp(escapeRegExp(uiText), "g"),
"\n",
)
}
return sanitizedText
.split("\n")
.map((line) => line.trim())
.filter((line) => line)
.join("\n")
.trim()
}
function getGrokMemoryText(dialog: HTMLElement): string {
const clonedDialog = dialog.cloneNode(true) as HTMLElement
clonedDialog.querySelector("#supermemory-save-button")?.remove()
const possibleMemoryContainers = Array.from(
clonedDialog.querySelectorAll<HTMLElement>(
"article, section, [class*='overflow'], [class*='prose'], [class*='whitespace']",
),
)
.map((element) => element.innerText || element.textContent || "")
.map(sanitizeGrokMemoryText)
.filter((text) => text.length > 30)
.sort((a, b) => b.length - a.length)
if (possibleMemoryContainers[0]) {
return possibleMemoryContainers[0]
}
return sanitizeGrokMemoryText(
clonedDialog.innerText || clonedDialog.textContent || "",
)
}
function createSupermemoryButton(memoryDialog: HTMLElement) {
const supermemoryButton = document.createElement("button")
supermemoryButton.id = "supermemory-save-button"
const iconUrl = browser.runtime.getURL("/icon-16.png")
supermemoryButton.innerHTML = `
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 8px; white-space: nowrap;">
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
<span style="white-space: nowrap;">Save to supermemory</span>
</div>
`
supermemoryButton.style.cssText = `
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: auto !important;
min-width: 190px !important;
background: #1C2026 !important;
color: white !important;
border: 1px solid #1C2026 !important;
border-radius: 9999px !important;
padding: 10px 16px !important;
font-weight: 500 !important;
font-size: 14px !important;
line-height: 20px !important;
white-space: nowrap !important;
cursor: pointer !important;
font-family: inherit !important;
z-index: 1 !important;
`
supermemoryButton.addEventListener("mouseenter", () => {
supermemoryButton.style.backgroundColor = "#2B2E33"
})
supermemoryButton.addEventListener("mouseleave", () => {
supermemoryButton.style.backgroundColor = "#1C2026"
})
supermemoryButton.addEventListener("click", async () => {
await saveGrokMemoriesToSupermemory(memoryDialog)
})
return supermemoryButton
}
function addSupermemoryButtonToGrokMemoryDialog() {
const memoryDialog = getGrokMemoryDialog()
if (!memoryDialog) return
if (memoryDialog.querySelector("#supermemory-save-button")) return
const supermemoryButton = createSupermemoryButton(memoryDialog)
const heading = Array.from(memoryDialog.querySelectorAll("h1, h2, h3")).find(
(element) => element.textContent?.trim() === "Memory from your chats",
)
const closeButton = Array.from(
memoryDialog.querySelectorAll<HTMLButtonElement>("button"),
).find((button) => {
const label = button.getAttribute("aria-label")?.toLowerCase() || ""
const text = button.textContent?.trim().toLowerCase() || ""
return label.includes("close") || text === "×" || text === "x"
})
if (heading?.parentElement) {
const header = heading.parentElement
header.style.display = "flex"
header.style.alignItems = "center"
header.style.gap = "12px"
const spacer = document.createElement("div")
spacer.style.flex = "1"
if (closeButton?.parentElement === header) {
header.insertBefore(spacer, closeButton)
header.insertBefore(supermemoryButton, closeButton)
} else {
header.appendChild(spacer)
header.appendChild(supermemoryButton)
}
return
}
if (closeButton?.parentElement) {
closeButton.parentElement.insertBefore(supermemoryButton, closeButton)
return
}
memoryDialog.insertBefore(supermemoryButton, memoryDialog.firstChild)
}
async function saveGrokMemoriesToSupermemory(memoryDialog: HTMLElement) {
try {
DOMUtils.showToast("loading")
const memoryText = getGrokMemoryText(memoryDialog)
if (!memoryText) {
DOMUtils.showToast("error")
return
}
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
content: memoryText,
title: "Grok memories import",
},
actionSource: "grok_memories_dialog",
})
if (response.success) {
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving Grok memories to supermemory:", error)
DOMUtils.showToast("error")
}
}

View file

@ -0,0 +1,83 @@
import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
import { DOMUtils } from "../../utils/ui-components"
import { initializeChatGPT } from "./chatgpt"
import { initializeClaude } from "./claude"
import { initializeGrok } from "./grok"
import {
saveMemory,
setupGlobalKeyboardShortcut,
setupStorageListener,
} from "./shared"
import { initializeT3 } from "./t3"
import {
handleTwitterNavigation,
initializeTwitter,
openImportModal,
updateTwitterImportUI,
} from "./twitter"
export default defineContentScript({
matches: ["<all_urls>"],
main() {
// Setup global event listeners
browser.runtime.onMessage.addListener(async (message) => {
if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
DOMUtils.showToast(message.state)
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
await saveMemory()
} else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) {
await openImportModal()
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
updateTwitterImportUI(message)
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
updateTwitterImportUI(message)
}
})
// Setup global keyboard shortcuts
setupGlobalKeyboardShortcut()
// Setup storage listener
setupStorageListener()
// Observer for dynamic content changes
const observeForDynamicChanges = () => {
const observer = new MutationObserver(() => {
if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
initializeChatGPT()
}
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
initializeClaude()
}
if (DOMUtils.isOnDomain(DOMAINS.GROK)) {
initializeGrok()
}
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
initializeT3()
}
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
handleTwitterNavigation()
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
})
}
// Initialize platform-specific functionality
initializeChatGPT()
initializeClaude()
initializeGrok()
initializeT3()
initializeTwitter()
// Start observing for dynamic changes
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", observeForDynamicChanges)
} else {
observeForDynamicChanges()
}
},
})

View file

@ -0,0 +1,129 @@
import { MESSAGE_TYPES } from "../../utils/constants"
import { bearerToken, userData } from "../../utils/storage"
import { DOMUtils } from "../../utils/ui-components"
import { default as TurndownService } from "turndown"
export async function saveMemory() {
try {
DOMUtils.showToast("loading")
const highlightedText = window.getSelection()?.toString() || ""
const url = window.location.href
const ogImage =
document
.querySelector('meta[property="og:image"]')
?.getAttribute("content") ||
document
.querySelector('meta[name="og:image"]')
?.getAttribute("content") ||
undefined
const title =
document
.querySelector('meta[property="og:title"]')
?.getAttribute("content") ||
document
.querySelector('meta[name="og:title"]')
?.getAttribute("content") ||
document.title ||
undefined
const data: {
html?: string
markdown?: string
highlightedText?: string
url: string
ogImage?: string
title?: string
} = {
url,
}
if (ogImage) {
data.ogImage = ogImage
}
if (title) {
data.title = title
}
if (highlightedText) {
data.highlightedText = highlightedText
} else {
const bodyClone = document.body.cloneNode(true) as HTMLElement
const scripts = bodyClone.querySelectorAll("script")
for (const script of scripts) {
script.remove()
}
const html = bodyClone.innerHTML
// Convert HTML to markdown
const turndownService = new TurndownService()
const markdown = turndownService.turndown(html)
data.markdown = markdown
}
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data,
actionSource: "context_menu",
})
console.log("Response from enxtension:", response)
if (response.success) {
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving memory:", error)
DOMUtils.showToast("error")
}
}
export function setupGlobalKeyboardShortcut() {
document.addEventListener("keydown", async (event) => {
if (
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "m"
) {
event.preventDefault()
await saveMemory()
}
})
}
export function setupStorageListener() {
window.addEventListener("message", async (event) => {
if (event.source !== window) {
return
}
const token = event.data.token
const user = event.data.userData
if (token && user) {
if (
!(
window.location.hostname === "localhost" ||
window.location.hostname === "supermemory.ai" ||
window.location.hostname === "app.supermemory.ai"
)
) {
console.log(
"Bearer token and user data is only allowed to be used on localhost or supermemory.ai",
)
return
}
try {
await Promise.all([
bearerToken.setValue(token),
userData.setValue(user),
])
} catch {
// Do nothing
}
}
})
}

View file

@ -0,0 +1,731 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
UI_CONFIG,
} from "../../utils/constants"
import {
autoSearchEnabled,
autoCapturePromptsEnabled,
} from "../../utils/storage"
import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components"
let t3DebounceTimeout: NodeJS.Timeout | null = null
let t3RouteObserver: MutationObserver | null = null
let t3UrlCheckInterval: NodeJS.Timeout | null = null
let t3ObserverThrottle: NodeJS.Timeout | null = null
export function initializeT3() {
if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
return
}
if (document.body.hasAttribute("data-t3-initialized")) {
return
}
setTimeout(() => {
console.log("Adding supermemory icon to T3 input")
addSupermemoryIconToT3Input()
setupT3AutoFetch()
}, 2000)
setupT3PromptCapture()
setupT3RouteChangeDetection()
document.body.setAttribute("data-t3-initialized", "true")
}
function setupT3RouteChangeDetection() {
if (t3RouteObserver) {
t3RouteObserver.disconnect()
}
if (t3UrlCheckInterval) {
clearInterval(t3UrlCheckInterval)
}
if (t3ObserverThrottle) {
clearTimeout(t3ObserverThrottle)
t3ObserverThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log("T3 route changed, re-adding supermemory icon")
setTimeout(() => {
addSupermemoryIconToT3Input()
setupT3AutoFetch()
}, 1000)
}
}
t3UrlCheckInterval = setInterval(checkForRouteChange, 2000)
t3RouteObserver = new MutationObserver((mutations) => {
if (t3ObserverThrottle) {
return
}
let shouldRecheck = false
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (
element.querySelector?.("textarea") ||
element.querySelector?.('div[contenteditable="true"]') ||
element.matches?.("textarea") ||
element.matches?.('div[contenteditable="true"]')
) {
shouldRecheck = true
}
}
})
}
})
if (shouldRecheck) {
t3ObserverThrottle = setTimeout(() => {
try {
t3ObserverThrottle = null
addSupermemoryIconToT3Input()
setupT3AutoFetch()
} catch (error) {
console.error("Error in T3 observer callback:", error)
}
}, 300)
}
})
try {
t3RouteObserver.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error("Failed to set up T3 route observer:", error)
if (t3UrlCheckInterval) {
clearInterval(t3UrlCheckInterval)
}
t3UrlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
function addSupermemoryIconToT3Input() {
const targetContainers = document.querySelectorAll(
".flex.min-w-0.items-center.gap-2",
)
const container = targetContainers[0]
if (!container) {
return
}
if (container.hasAttribute("data-supermemory-icon-added")) {
return
}
const existingIcon = container.querySelector(
`#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`,
)
if (existingIcon) {
container.setAttribute("data-supermemory-icon-added", "true")
return
}
const supermemoryIcon = createT3InputBarElement(async () => {
await getRelatedMemoriesForT3(POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED)
})
supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true")
container.insertBefore(supermemoryIcon, container.firstChild)
}
async function getRelatedMemoriesForT3(actionSource: string) {
try {
let userQuery = ""
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (supermemoryContainer?.parentElement?.previousElementSibling) {
const textareaElement =
supermemoryContainer.parentElement.previousElementSibling.querySelector(
"textarea",
)
userQuery = textareaElement?.value || ""
}
if (!userQuery.trim()) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
userQuery =
textareaElement?.innerText || textareaElement?.textContent || ""
}
if (!userQuery.trim()) {
const textareas = document.querySelectorAll("textarea")
for (const textarea of textareas) {
const text = (textarea as HTMLTextAreaElement).value
if (text?.trim()) {
userQuery = text.trim()
break
}
}
}
console.log("T3 query extracted:", userQuery)
if (!userQuery.trim()) {
console.log("No query text found for T3")
return
}
const icon = document.querySelector('[id*="sm-t3-input-bar-element"]')
const iconElement = icon as HTMLElement
if (!iconElement) {
console.warn("T3 icon element not found, cannot update feedback")
return
}
updateT3IconFeedback("Searching memories...", iconElement)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Memory search timeout")),
UI_CONFIG.API_REQUEST_TIMEOUT,
),
)
const response = await Promise.race([
browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: actionSource,
}),
timeoutPromise,
])
console.log("T3 memories response:", response)
if (response?.success && response?.data) {
let textareaElement = null
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (supermemoryContainer?.parentElement?.previousElementSibling) {
textareaElement =
supermemoryContainer.parentElement.previousElementSibling.querySelector(
"textarea",
)
}
if (!textareaElement) {
textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
}
if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}`
iconElement.dataset.memoriesData = response.data
updateT3IconFeedback("Included Memories", iconElement)
} else {
console.warn("T3 input area not found after successful memory fetch")
updateT3IconFeedback("Memories found", iconElement)
}
} else {
console.warn("No memories found or API response invalid for T3")
updateT3IconFeedback("No memories found", iconElement)
}
} catch (error) {
console.error("Error getting related memories for T3:", error)
try {
const icon = document.querySelector(
'[id*="sm-t3-input-bar-element"]',
) as HTMLElement
if (icon) {
updateT3IconFeedback("Error fetching memories", icon)
}
} catch (feedbackError) {
console.error("Failed to update T3 error feedback:", feedbackError)
}
}
}
function updateT3IconFeedback(
message: string,
iconElement: HTMLElement,
resetAfter = 0,
) {
if (!iconElement.dataset.originalHtml) {
iconElement.dataset.originalHtml = iconElement.innerHTML
}
const feedbackDiv = document.createElement("div")
feedbackDiv.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: #513EA9;
border-radius: 6px;
color: white;
font-size: 12px;
font-weight: 500;
cursor: ${message === "Included Memories" ? "pointer" : "default"};
position: relative;
`
feedbackDiv.innerHTML = `
<span></span>
<span>${message}</span>
`
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
const popup = document.createElement("div")
popup.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
padding: 0;
border-radius: 12px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
max-width: 500px;
max-height: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 999999;
display: none;
border: 1px solid #333;
`
const header = document.createElement("div")
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
`
header.innerHTML = `
<span style="font-weight: 600; color: #fff;">Included Memories</span>
`
const content = document.createElement("div")
content.style.cssText = `
padding: 0;
max-height: 300px;
overflow-y: auto;
`
const memoriesText = iconElement.dataset.memoriesData || ""
console.log("Memories text:", memoriesText)
const individualMemories = memoriesText
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
console.log("Individual memories:", individualMemories)
individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div")
memoryItem.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 10px;
font-size: 13px;
line-height: 1.4;
`
const memoryText = document.createElement("div")
memoryText.style.cssText = `
flex: 1;
color: #e5e5e5;
`
memoryText.textContent = memory.trim()
const removeBtn = document.createElement("button")
removeBtn.style.cssText = `
background: transparent;
color: #9ca3af;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
`
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
removeBtn.dataset.memoryIndex = index.toString()
removeBtn.addEventListener("mouseenter", () => {
removeBtn.style.color = "#ef4444"
})
removeBtn.addEventListener("mouseleave", () => {
removeBtn.style.color = "#9ca3af"
})
memoryItem.appendChild(memoryText)
memoryItem.appendChild(removeBtn)
content.appendChild(memoryItem)
})
popup.appendChild(header)
popup.appendChild(content)
document.body.appendChild(popup)
feedbackDiv.addEventListener("mouseenter", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Click to see memories"
}
})
feedbackDiv.addEventListener("mouseleave", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Included Memories"
}
})
feedbackDiv.addEventListener("click", (e) => {
e.stopPropagation()
popup.style.display = "block"
})
document.addEventListener("click", (e) => {
if (!popup.contains(e.target as Node)) {
popup.style.display = "none"
}
})
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
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 textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}`
}
content
.querySelectorAll("button[data-memory-index]")
.forEach((btn, newIndex) => {
const htmlBtn = btn as HTMLButtonElement
htmlBtn.dataset.memoryIndex = newIndex.toString()
})
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 = ""
iconElement.appendChild(feedbackDiv)
if (resetAfter > 0) {
setTimeout(() => {
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}, resetAfter)
}
}
function setupT3PromptCapture() {
if (document.body.hasAttribute("data-t3-prompt-capture-setup")) {
return
}
document.body.setAttribute("data-t3-prompt-capture-setup", "true")
const captureT3PromptContent = async (source: string) => {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) {
console.log("Auto capture prompts is disabled, skipping prompt capture")
return
}
let promptContent = ""
const textarea = document.querySelector("textarea") as HTMLTextAreaElement
if (textarea) {
promptContent = textarea.value || ""
}
if (!promptContent) {
const contentEditableDiv = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (contentEditableDiv) {
promptContent =
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
}
}
const textareaElement =
textarea ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
const storedMemories = textareaElement?.dataset.supermemories
if (
storedMemories &&
textareaElement &&
!promptContent.includes("Supermemories of user")
) {
if (textareaElement.tagName === "TEXTAREA") {
;(textareaElement as HTMLTextAreaElement).value =
`${promptContent} ${storedMemories}`
promptContent = (textareaElement as HTMLTextAreaElement).value
} else {
textareaElement.appendChild(document.createTextNode(storedMemories))
promptContent =
textareaElement.textContent || textareaElement.innerText || ""
}
}
if (promptContent.trim()) {
console.log(`T3 prompt submitted via ${source}:`, promptContent)
try {
await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "t3",
source: source,
},
})
} catch (error) {
console.error("Error sending T3 prompt to background:", error)
}
}
const icons = document.querySelectorAll('[id*="sm-t3-input-bar-element"]')
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
}
const handleT3SendButtonClick = async (event: Event) => {
const target = event.target as HTMLElement
const sendButton =
target.closest("button.focus-visible\\:ring-ring") ||
target.closest('button[class*="bg-[rgb(162,59,103)]"]') ||
target.closest('button[class*="rounded-lg"]')
if (sendButton) {
const textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
const hasMemories =
textareaElement?.dataset.supermemories ||
(
document.querySelector(
'[id*="sm-t3-input-bar-element"]',
) as HTMLElement
)?.dataset.memoriesData
if (!hasMemories) {
return // No memories present, let the button click proceed normally
}
event.preventDefault()
event.stopPropagation()
await captureT3PromptContent("button click")
setTimeout(() => {
const form = sendButton.closest("form")
if (form) {
form.requestSubmit()
} else {
const newEvent = new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window,
})
document.removeEventListener("click", handleT3SendButtonClick, true)
sendButton.dispatchEvent(newEvent)
setTimeout(() => {
document.addEventListener("click", handleT3SendButtonClick, true)
}, 100)
}
}, 100)
}
}
const handleT3EnterKey = async (event: KeyboardEvent) => {
const target = event.target as HTMLElement
if (
(target.matches("textarea") ||
target.matches('div[contenteditable="true"]')) &&
event.key === "Enter" &&
!event.shiftKey
) {
const textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
const hasMemories =
textareaElement?.dataset.supermemories ||
(
document.querySelector(
'[id*="sm-t3-input-bar-element"]',
) as HTMLElement
)?.dataset.memoriesData
if (!hasMemories) {
return // No memories present, let the Enter key proceed normally
}
event.preventDefault()
event.stopPropagation()
await captureT3PromptContent("Enter key")
setTimeout(() => {
const form = target.closest("form")
if (form) {
form.requestSubmit()
} else {
const newEvent = new KeyboardEvent("keydown", {
key: "Enter",
code: "Enter",
bubbles: true,
cancelable: true,
})
target.dispatchEvent(newEvent)
}
}, 100)
}
}
document.addEventListener("click", handleT3SendButtonClick, true)
document.addEventListener("keydown", handleT3EnterKey, true)
}
async function setupT3AutoFetch() {
const autoSearch = (await autoSearchEnabled.getValue()) ?? false
if (!autoSearch) {
return
}
const textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
if (
!textareaElement ||
textareaElement.hasAttribute("data-supermemory-auto-fetch")
) {
return
}
textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => {
if (t3DebounceTimeout) {
clearTimeout(t3DebounceTimeout)
}
t3DebounceTimeout = setTimeout(async () => {
let content = ""
if (textareaElement.tagName === "TEXTAREA") {
content = (textareaElement as HTMLTextAreaElement).value?.trim() || ""
} else {
content = textareaElement.textContent?.trim() || ""
}
if (content.length > 2) {
await getRelatedMemoriesForT3(
POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_AUTO_SEARCHED,
)
} else if (content.length === 0) {
const icons = document.querySelectorAll(
'[id*="sm-t3-input-bar-element"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (textareaElement.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
}
textareaElement.addEventListener("input", handleInput)
}

View file

@ -0,0 +1,739 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
STORAGE_KEYS,
UI_CONFIG,
} from "../../utils/constants"
import { trackEvent } from "../../utils/posthog"
import {
createProjectSelectionModal,
createSaveTweetElement,
DOMUtils,
} from "../../utils/ui-components"
async function loadSpaceGroteskFonts(): Promise<void> {
if (document.getElementById("supermemory-modal-styles")) {
return Promise.resolve()
}
const style = document.createElement("style")
style.id = "supermemory-modal-styles"
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap');
`
document.head.appendChild(style)
await document.fonts.ready
}
/**
* Check if import intent is valid (exists and not expired)
*/
async function checkAndConsumeImportIntent(): Promise<boolean> {
try {
const result = await browser.storage.local.get(
STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL,
)
const intentUntil = result[
STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL
] as number | undefined
if (intentUntil && Date.now() < intentUntil) {
await browser.storage.local.remove(
STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL,
)
return true
}
return false
} catch (error) {
console.error("Error checking import intent:", error)
return false
}
}
/**
* Check if onboarding toast has been shown before
*/
async function hasOnboardingBeenShown(): Promise<boolean> {
try {
const result = await browser.storage.local.get(
STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN,
)
return !!result[STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN]
} catch (error) {
console.error("Error checking onboarding status:", error)
return true // Default to true to avoid showing toast on error
}
}
/**
* Mark onboarding toast as shown
*/
async function markOnboardingAsShown(): Promise<void> {
try {
await browser.storage.local.set({
[STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN]: true,
})
} catch (error) {
console.error("Error marking onboarding as shown:", error)
}
}
export async function initializeTwitter() {
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
return
}
if (window.location.pathname === "/i/bookmarks") {
setTimeout(async () => {
if (window.location.pathname === "/i/bookmarks") {
await handleBookmarksPageLoad()
}
}, 2000)
} else {
// Clean up any injected UI if navigating away
removeAllTwitterUI()
}
}
/**
* Handle what to show when user lands on bookmarks page
*/
async function handleBookmarksPageLoad() {
if (window.location.pathname !== "/i/bookmarks") {
return
}
addTwitterImportButtonForFolders() // Add buttons to bookmark folders
const hasIntent = await checkAndConsumeImportIntent()
if (hasIntent) {
await openImportModal()
return
}
const onboardingShown = await hasOnboardingBeenShown()
if (!onboardingShown) {
await showOnboardingToast()
await markOnboardingAsShown()
}
}
/**
* Opens the import modal and handles the import flow
*/
export async function openImportModal() {
try {
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.FETCH_PROJECTS,
})
const projects = response.success && response.data ? response.data : []
if (projects.length === 0) {
await browser.runtime.sendMessage({
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
})
await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
})
} else {
await showAllBookmarksProjectModal(projects)
}
} catch (error) {
console.error("Error opening import modal:", error)
await browser.runtime.sendMessage({
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
})
}
}
async function showAllBookmarksProjectModal(
projects: Array<{ id: string; name: string; containerTag: string }>,
) {
await loadSpaceGroteskFonts()
const modal = createProjectSelectionModal(
projects,
async (selectedProject) => {
modal.remove()
try {
await browser.runtime.sendMessage({
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
selectedProject: selectedProject,
})
await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
project_selected: true,
})
} catch (error) {
console.error("Error importing all bookmarks:", error)
}
},
() => {
modal.remove()
},
)
document.body.appendChild(modal)
}
/**
* Shows the one-time onboarding toast with progress bar
*/
async function showOnboardingToast() {
await loadSpaceGroteskFonts()
// Remove any existing toast
const existingToast = document.getElementById(
ELEMENT_IDS.TWITTER_ONBOARDING_TOAST,
)
if (existingToast) {
existingToast.remove()
}
const duration = UI_CONFIG.ONBOARDING_TOAST_DURATION
// Create toast container
const toast = document.createElement("div")
toast.id = ELEMENT_IDS.TWITTER_ONBOARDING_TOAST
toast.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 2147483647;
background: #ffffff;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
color: #374151;
min-width: 320px;
max-width: 380px;
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12);
animation: smSlideInUp 0.3s ease-out;
overflow: hidden;
`
// Add keyframe animations if not already present
if (!document.getElementById("supermemory-onboarding-toast-styles")) {
const style = document.createElement("style")
style.id = "supermemory-onboarding-toast-styles"
style.textContent = `
@keyframes smSlideInUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes smFadeOut {
from { transform: translateY(0); opacity: 1; }
to { transform: translateY(100%); opacity: 0; }
}
@keyframes smProgressGrow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
@keyframes smPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
`
document.head.appendChild(style)
}
// Header with icon, text and close button
const header = document.createElement("div")
header.style.cssText =
"display: flex; align-items: flex-start; gap: 12px; position: relative;"
const iconUrl = browser.runtime.getURL("/icon-16.png")
const icon = document.createElement("img")
icon.src = iconUrl
icon.alt = "Supermemory"
icon.style.cssText =
"width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;"
const textContainer = document.createElement("div")
textContainer.style.cssText =
"display: flex; flex-direction: column; gap: 4px; flex: 1;"
const title = document.createElement("span")
title.style.cssText = "font-weight: 600; font-size: 14px; color: #111827;"
title.textContent = "Import X/Twitter Bookmarks"
const description = document.createElement("span")
description.style.cssText =
"font-size: 13px; color: #6b7280; line-height: 1.4;"
description.textContent =
"You can import all your Twitter bookmarks to Supermemory with one click."
textContainer.appendChild(title)
textContainer.appendChild(description)
// Close button
const closeButton = document.createElement("button")
closeButton.setAttribute("aria-label", "Close onboarding toast")
closeButton.style.cssText = `
position: absolute;
top: 0;
right: 0;
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
color: #9ca3af;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
`
closeButton.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
`
closeButton.addEventListener("mouseenter", () => {
closeButton.style.backgroundColor = "#f3f4f6"
})
closeButton.addEventListener("mouseleave", () => {
closeButton.style.backgroundColor = "transparent"
})
closeButton.addEventListener("click", () => {
dismissToast(toast)
})
header.appendChild(icon)
header.appendChild(textContainer)
header.appendChild(closeButton)
// Action buttons
const buttonsContainer = document.createElement("div")
buttonsContainer.style.cssText = "display: flex; gap: 8px; margin-top: 4px;"
const importButton = document.createElement("button")
importButton.style.cssText = `
padding: 8px 16px;
border: none;
border-radius: 8px;
background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%);
color: white;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
font-family: inherit;
`
importButton.textContent = "Import now"
importButton.addEventListener("mouseenter", () => {
importButton.style.opacity = "0.9"
})
importButton.addEventListener("mouseleave", () => {
importButton.style.opacity = "1"
})
importButton.addEventListener("click", async () => {
dismissToast(toast)
await openImportModal()
})
const learnMoreButton = document.createElement("button")
learnMoreButton.style.cssText = `
padding: 8px 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: transparent;
color: #374151;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
font-family: inherit;
`
learnMoreButton.textContent = "Learn more"
learnMoreButton.addEventListener("mouseenter", () => {
learnMoreButton.style.backgroundColor = "#f9fafb"
})
learnMoreButton.addEventListener("mouseleave", () => {
learnMoreButton.style.backgroundColor = "transparent"
})
learnMoreButton.addEventListener("click", () => {
window.open("https://docs.supermemory.ai/connectors/twitter", "_blank")
})
buttonsContainer.appendChild(importButton)
buttonsContainer.appendChild(learnMoreButton)
// Progress bar container
const progressBarContainer = document.createElement("div")
progressBarContainer.setAttribute("role", "progressbar")
progressBarContainer.setAttribute("aria-valuemin", "0")
progressBarContainer.setAttribute("aria-valuemax", "100")
progressBarContainer.setAttribute("aria-valuenow", "0")
progressBarContainer.setAttribute(
"aria-label",
"Onboarding toast auto-dismiss progress",
)
progressBarContainer.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: #e5e7eb;
`
const progressBar = document.createElement("div")
progressBar.style.cssText = `
height: 100%;
background: linear-gradient(90deg, #0ff0d2, #5bd3fb, #1e0ff0);
transform-origin: left;
animation: smProgressGrow ${duration}ms linear forwards;
`
// Update progress bar ARIA value as animation progresses
const startTime = Date.now()
const updateProgress = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(100, Math.round((elapsed / duration) * 100))
progressBarContainer.setAttribute("aria-valuenow", String(progress))
if (progress < 100) {
requestAnimationFrame(updateProgress)
}
}
requestAnimationFrame(updateProgress)
progressBarContainer.appendChild(progressBar)
// Assemble toast
toast.appendChild(header)
toast.appendChild(buttonsContainer)
toast.appendChild(progressBarContainer)
document.body.appendChild(toast)
// Auto-dismiss after duration
setTimeout(() => {
if (document.body.contains(toast)) {
dismissToast(toast)
}
}, duration)
}
/**
* Dismiss the toast with animation
*/
function dismissToast(toast: HTMLElement) {
toast.style.animation = "smFadeOut 0.3s ease-out forwards"
setTimeout(() => {
if (document.body.contains(toast)) {
toast.remove()
}
}, 300)
}
/**
* Remove all Twitter-specific injected UI
*/
function removeAllTwitterUI() {
// Remove import button (legacy)
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
}
// Remove onboarding toast
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST)
}
// Remove import progress toast
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)
}
// Remove any folder buttons
document.querySelectorAll("[data-supermemory-button]").forEach((button) => {
button.remove()
})
}
/**
* Shows or updates the import progress toast in the bottom-right
*/
function showOrUpdateImportProgressToast(message: string, isComplete = false) {
let toast = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)
if (!toast) {
// Ensure animation styles are available
if (!document.getElementById("supermemory-onboarding-toast-styles")) {
const style = document.createElement("style")
style.id = "supermemory-onboarding-toast-styles"
style.textContent = `
@keyframes smSlideInUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes smFadeOut {
from { transform: translateY(0); opacity: 1; }
to { transform: translateY(100%); opacity: 0; }
}
@keyframes smPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
`
document.head.appendChild(style)
}
// Create new toast
toast = document.createElement("div")
toast.id = ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST
toast.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 2147483647;
background: #ffffff;
border-radius: 12px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 12px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
color: #374151;
min-width: 280px;
max-width: 360px;
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12);
animation: smSlideInUp 0.3s ease-out;
`
const iconUrl = browser.runtime.getURL("/icon-16.png")
const icon = document.createElement("img")
icon.src = iconUrl
icon.alt = "Supermemory"
icon.id = "sm-import-progress-icon"
icon.style.cssText =
"width: 20px; height: 20px; border-radius: 4px; flex-shrink: 0; animation: smPulse 1.5s ease-in-out infinite;"
const textSpan = document.createElement("span")
textSpan.id = "sm-import-progress-text"
textSpan.style.cssText = "font-weight: 500; flex: 1;"
textSpan.textContent = message
toast.appendChild(icon)
toast.appendChild(textSpan)
document.body.appendChild(toast)
} else {
// Update existing toast
const textSpan = toast.querySelector(
"#sm-import-progress-text",
) as HTMLSpanElement
if (textSpan) {
textSpan.textContent = message
}
}
// Style for completion
if (isComplete) {
const icon = toast.querySelector(
"#sm-import-progress-icon",
) as HTMLImageElement
if (icon) {
icon.style.animation = "none"
icon.style.opacity = "1"
}
const textSpan = toast.querySelector(
"#sm-import-progress-text",
) as HTMLSpanElement
if (textSpan) {
textSpan.style.color = "#059669"
}
// Auto-dismiss after 4 seconds on completion
setTimeout(() => {
const existingToast = document.getElementById(
ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST,
)
if (existingToast) {
dismissToast(existingToast)
}
}, 4000)
}
}
export function updateTwitterImportUI(message: {
type: string
importedMessage?: string
totalImported?: number
}) {
if (message.type === MESSAGE_TYPES.IMPORT_UPDATE && message.importedMessage) {
showOrUpdateImportProgressToast(message.importedMessage, false)
}
if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
showOrUpdateImportProgressToast(
`✓ Imported ${message.totalImported} tweets!`,
true,
)
}
}
export async function handleTwitterNavigation() {
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
return
}
if (window.location.pathname === "/i/bookmarks") {
addTwitterImportButtonForFolders()
await handleBookmarksPageLoad()
} else {
removeAllTwitterUI()
}
}
/**
* Adds import buttons to bookmark folders
*/
function addTwitterImportButtonForFolders() {
if (window.location.pathname !== "/i/bookmarks") {
return
}
const targetElements = document.querySelectorAll(
".css-175oi2r.r-1wtj0ep.r-16x9es5.r-1mmae3n.r-o7ynqc.r-6416eg.r-1ny4l3l.r-1loqt21",
)
targetElements.forEach((element) => {
addButtonToElement(element as HTMLElement)
})
}
/**
* Adds an import button to a bookmark folder element
*/
function addButtonToElement(element: HTMLElement) {
if (element.querySelector("[data-supermemory-button]")) {
return
}
loadSpaceGroteskFonts()
const button = createSaveTweetElement(async () => {
const url = element.getAttribute("href")
const bookmarkCollectionId = url?.split("/").pop()
if (bookmarkCollectionId) {
await showFolderProjectSelectionModal(bookmarkCollectionId)
}
})
button.setAttribute("data-supermemory-button", "true")
element.appendChild(button)
element.style.flexDirection = "row"
element.style.alignItems = "center"
element.style.justifyContent = "center"
element.style.gap = "10px"
element.style.padding = "10px"
}
/**
* Shows the project selection modal for folder imports
*/
async function showFolderProjectSelectionModal(bookmarkCollectionId: string) {
await loadSpaceGroteskFonts()
const modal = createProjectSelectionModal(
[],
async (selectedProject) => {
modal.remove()
try {
await browser.runtime.sendMessage({
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
isFolderImport: true,
bookmarkCollectionId: bookmarkCollectionId,
selectedProject: selectedProject,
})
} catch (error) {
console.error("Error importing bookmarks:", error)
}
},
() => {
modal.remove()
},
)
document.body.appendChild(modal)
try {
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.FETCH_PROJECTS,
})
if (response.success && response.data) {
const projects = response.data
updateModalWithProjects(modal, projects)
} else {
console.error("Failed to fetch projects:", response.error)
updateModalWithProjects(modal, [])
}
} catch (error) {
console.error("Error fetching projects:", error)
updateModalWithProjects(modal, [])
}
}
/**
* Updates the modal with fetched projects
*/
function updateModalWithProjects(
modal: HTMLElement,
projects: Array<{ id: string; name: string; containerTag: string }>,
) {
const select = modal.querySelector("#project-select") as HTMLSelectElement
if (!select) return
while (select.children.length > 1) {
select.removeChild(select.children[1])
}
if (projects.length === 0) {
const noProjectsOption = document.createElement("option")
noProjectsOption.value = ""
noProjectsOption.textContent = "No projects available"
noProjectsOption.disabled = true
select.appendChild(noProjectsOption)
const importButton = modal.querySelector(
"button:last-child",
) as HTMLButtonElement
if (importButton) {
importButton.disabled = true
importButton.style.cssText = `
padding: 10px 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.3);
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
transition: all 0.2s ease;
`
}
} else {
projects.forEach((project) => {
const option = document.createElement("option")
option.value = project.id
option.textContent = project.name
option.dataset.containerTag = project.containerTag
select.appendChild(option)
})
}
}

View file

@ -0,0 +1,42 @@
@import "tailwindcss";
/* Custom Font Definitions */
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("/fonts/SpaceGrotesk-Light.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/SpaceGrotesk-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/fonts/SpaceGrotesk-Medium.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/fonts/SpaceGrotesk-SemiBold.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/fonts/SpaceGrotesk-Bold.ttf") format("truetype");
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Default Popup Title</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,17 @@
import { QueryClientProvider } from "@tanstack/react-query"
import React from "react"
import ReactDOM from "react-dom/client"
import { queryClient } from "../../utils/query-client"
import App from "./App.js"
import "./style.css"
const rootElement = document.getElementById("root")
if (rootElement) {
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>,
)
}

View file

@ -0,0 +1,21 @@
:root {
font-family:
"Space Grotesk", Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: dark;
color: rgba(255, 255, 255, 0.92);
background-color: #0a0e14;
border: 1px solid #0a0e14;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a:hover {
color: #93c5fd;
}

View file

@ -0,0 +1,107 @@
function Welcome() {
return (
<div className="min-h-screen font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif] flex items-center justify-center p-8 bg-gradient-to-br from-gray-50 to-white">
<div className="max-w-4xl w-full text-center">
{/* Header */}
<div className="mb-12">
<img
alt="supermemory"
className="h-16 mb-6 mx-auto"
src="https://assets.supermemory.ai/brand/wordmark/dark-transparent.svg"
/>
<p className="text-gray-600 text-lg font-normal max-w-2xl mx-auto">
Your AI second brain for saving and organizing everything that
matters. Supermemory learns and remembers everything you save, your
preferences, and understands you.
</p>
</div>
{/* Features Section */}
<div className="mb-12">
<h2 className="text-2xl font-semibold text-black mb-8">
What can you do with supermemory ?
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
<div className="text-3xl mb-4 block">💾</div>
<h3 className="text-lg font-semibold text-black mb-3">
Save Any Page
</h3>
<p className="text-sm text-gray-600 leading-snug">
Instantly save web pages, articles, and content to your personal
knowledge base
</p>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
<div className="text-3xl mb-4 block">🐦</div>
<h3 className="text-lg font-semibold text-black mb-3">
Import Twitter/X Bookmarks
</h3>
<p className="text-sm text-gray-600 leading-snug">
Bring all your saved tweets and bookmarks into one organized
place
</p>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
<div className="text-3xl mb-4 block">🤖</div>
<h3 className="text-lg font-semibold text-black mb-3">
Import ChatGPT Memories
</h3>
<p className="text-sm text-gray-600 leading-snug">
Keep your important AI conversations and insights accessible
</p>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-6 text-center transition-all duration-200 shadow-sm hover:-translate-y-0.5 hover:shadow-md hover:border-gray-300">
<div className="text-3xl mb-4 block">🔍</div>
<h3 className="text-lg font-semibold text-black mb-3">
Your context, everywhere.
</h3>
<p className="text-sm text-gray-600 leading-snug">
You can connect chatbots with MCP, chat with your personal
assistant, and more.
</p>
</div>
</div>
</div>
{/* Actions */}
<div className="mb-8">
<button
className="min-w-[200px] px-8 py-4 bg-gray-700 text-white border-none rounded-3xl text-base font-semibold cursor-pointer transition-colors duration-200 mb-4 outline-none hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
onClick={() => {
chrome.tabs.create({
url: import.meta.env.PROD
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
})
}}
type="button"
>
Login to Get started
</button>
</div>
{/* Footer */}
<div className="border-t border-gray-200 pt-6 mt-8">
<p className="text-sm text-gray-600">
Learn more at{" "}
<a
className="text-blue-500 no-underline hover:underline hover:text-blue-700"
href="https://supermemory.ai"
rel="noopener noreferrer"
target="_blank"
>
supermemory.ai
</a>
</p>
</div>
</div>
</div>
)
}
export default Welcome

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon-16.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to supermemory</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,17 @@
import { QueryClientProvider } from "@tanstack/react-query"
import React from "react"
import ReactDOM from "react-dom/client"
import { queryClient } from "../../utils/query-client"
import Welcome from "./Welcome"
import "./welcome.css"
const rootElement = document.getElementById("root")
if (rootElement) {
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<Welcome />
</QueryClientProvider>
</React.StrictMode>,
)
}

View file

@ -0,0 +1,49 @@
@import "tailwindcss";
/* Custom Font Definitions */
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("/fonts/SpaceGrotesk-Light.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/SpaceGrotesk-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/fonts/SpaceGrotesk-Medium.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/fonts/SpaceGrotesk-SemiBold.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/fonts/SpaceGrotesk-Bold.ttf") format("truetype");
}
/* Global Styles */
body {
font-family:
"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,35 @@
{
"name": "supermemory-browser-extension",
"description": "Browser extension for the supermemory app",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "wxt --port 3001",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.81.2",
"posthog-js": "^1.261.7",
"react": "19.2.2",
"react-dom": "19.2.2",
"tailwindcss": "^4.1.12",
"turndown": "^7.1.3"
},
"devDependencies": {
"@types/chrome": "^0.1.4",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"@types/turndown": "^5.0.5",
"@wxt-dev/module-react": "^1.1.3",
"typescript": "^5.8.3",
"wxt": "^0.20.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -0,0 +1,15 @@
<svg width="2560" height="512" viewBox="0 0 2560 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M527.06 228.982H410.629V128H373.012V237.567C373.012 249.205 377.616 260.381 385.798 268.615L480.867 364.283L507.466 337.517L437.249 266.858H527.082V229.004L527.06 228.982Z" fill="#1C2026"/>
<path d="M232.948 174.504L303.164 245.163H213.332V283.017H329.763V383.999H367.38V274.432C367.38 262.795 362.776 251.618 354.594 243.384L259.546 147.738L232.948 174.504Z" fill="#1C2026"/>
<path d="M715.99 330.047C697.748 330.047 682.78 326.072 671.128 318.124C659.455 310.175 652.407 298.823 649.963 284.046L682.693 275.527C684.002 282.158 686.227 287.362 689.326 291.138C692.424 294.937 696.286 297.616 700.89 299.24C705.494 300.843 710.535 301.656 715.99 301.656C724.259 301.656 730.369 300.185 734.318 297.264C738.268 294.322 740.253 290.699 740.253 286.33C740.253 281.96 738.377 278.622 734.602 276.251C730.827 273.88 724.827 271.947 716.535 270.432L708.636 269.005C698.861 267.117 689.915 264.504 681.842 261.188C673.747 257.873 667.266 253.284 662.378 247.421C657.491 241.559 655.047 233.983 655.047 224.717C655.047 210.708 660.131 199.971 670.277 192.484C680.445 185.018 693.777 181.264 710.338 181.264C725.94 181.264 738.922 184.777 749.265 191.759C759.608 198.764 766.372 207.942 769.579 219.294L736.566 229.504C735.06 222.324 732.005 217.208 727.401 214.178C722.797 211.148 717.103 209.633 710.338 209.633C703.574 209.633 698.381 210.818 694.824 213.19C691.246 215.561 689.457 218.833 689.457 222.983C689.457 227.528 691.333 230.887 695.108 233.061C698.861 235.235 703.945 236.904 710.338 238.023L718.237 239.451C728.776 241.339 738.311 243.842 746.865 246.982C755.418 250.1 762.182 254.557 767.179 260.332C772.154 266.107 774.663 273.924 774.663 283.761C774.663 298.516 769.339 309.934 758.713 317.97C748.087 326.028 733.838 330.047 715.968 330.047H715.99Z" fill="#1C2026"/>
<path d="M836.632 329.474C825.722 329.474 816.187 326.971 808.004 321.943C799.822 316.937 793.472 309.976 788.956 301.084C784.439 292.191 782.191 281.959 782.191 270.431V186.4H817.736V267.599C817.736 278.204 820.311 286.153 825.504 291.444C830.675 296.736 838.05 299.393 847.651 299.393C858.561 299.393 867.027 295.748 873.049 288.458C879.071 281.168 882.083 271.002 882.083 257.937V186.4H917.627V327.213H882.65V308.769H877.566C875.318 313.511 871.085 318.144 864.867 322.69C858.67 327.235 849.244 329.496 836.654 329.496L836.632 329.474Z" fill="#1C2026"/>
<path d="M943.004 383.992V186.398H977.981V203.437H983.065C986.251 197.948 991.248 193.073 998.012 188.813C1004.78 184.554 1014.46 182.424 1027.08 182.424C1038.36 182.424 1048.81 185.212 1058.39 190.79C1067.99 196.367 1075.69 204.557 1081.52 215.338C1087.34 226.119 1090.27 239.184 1090.27 254.51V259.055C1090.27 274.381 1087.34 287.446 1081.52 298.227C1075.69 309.008 1067.97 317.198 1058.39 322.775C1048.79 328.352 1038.36 331.141 1027.08 331.141C1018.61 331.141 1011.52 330.153 1005.78 328.155C1000.04 326.179 995.437 323.61 991.946 320.492C988.455 317.374 985.684 314.212 983.632 310.984H978.548V383.948H943.004V383.992ZM1016.36 299.961C1027.47 299.961 1036.63 296.404 1043.88 289.312C1051.12 282.22 1054.74 271.856 1054.74 258.221V255.388C1054.74 241.753 1051.08 231.389 1043.75 224.296C1036.41 217.204 1027.29 213.647 1016.38 213.647C1005.47 213.647 996.353 217.204 989.022 224.296C981.691 231.389 978.025 241.753 978.025 255.388V258.221C978.025 271.856 981.691 282.22 989.022 289.312C996.353 296.404 1005.47 299.961 1016.38 299.961H1016.36Z" fill="#1C2026"/>
<path d="M1172.66 331.185C1158.74 331.185 1146.47 328.199 1135.85 322.248C1125.22 316.276 1116.95 307.866 1111.02 296.975C1105.1 286.084 1102.14 273.261 1102.14 258.506V255.103C1102.14 240.347 1105.04 227.524 1110.89 216.633C1116.71 205.743 1124.89 197.333 1135.43 191.36C1145.97 185.41 1158.19 182.424 1172.11 182.424C1186.03 182.424 1197.79 185.498 1207.94 191.646C1218.09 197.794 1226.01 206.313 1231.64 217.204C1237.29 228.095 1240.1 240.721 1240.1 255.103V267.311H1138.25C1138.62 276.972 1142.2 284.811 1148.96 290.871C1155.73 296.931 1164.02 299.961 1173.79 299.961C1183.57 299.961 1191.1 297.788 1195.79 293.44C1200.48 289.093 1204.06 284.262 1206.5 278.97L1235.57 294.296C1232.92 299.215 1229.13 304.573 1224.13 310.347C1219.13 316.122 1212.52 321.04 1204.23 325.103C1195.96 329.165 1185.42 331.207 1172.64 331.207L1172.66 331.185ZM1138.51 240.611H1203.97C1203.21 232.465 1199.98 225.943 1194.24 221.025C1188.5 216.106 1181.02 213.647 1171.81 213.647C1162.6 213.647 1154.59 216.106 1148.96 221.025C1143.31 225.943 1139.84 232.487 1138.53 240.611H1138.51Z" fill="#1C2026"/>
<path d="M1257.58 327.211V186.399H1292.56V202.296H1297.64C1299.71 196.609 1303.14 192.459 1307.94 189.802C1312.74 187.146 1318.33 185.828 1324.72 185.828H1341.65V217.622H1324.15C1315.12 217.622 1307.7 220.038 1301.87 224.868C1296.05 229.699 1293.12 237.12 1293.12 247.155V327.211H1257.58Z" fill="#1C2026"/>
<path d="M1355.18 327.213V186.4H1390.16V201.726H1395.24C1397.68 197.006 1401.72 192.878 1407.37 189.386C1413.02 185.895 1420.44 184.139 1429.65 184.139C1439.62 184.139 1447.61 186.071 1453.63 189.957C1459.65 193.844 1464.26 198.894 1467.46 205.152H1472.55C1475.73 199.092 1480.25 194.085 1486.1 190.111C1491.92 186.137 1500.19 184.161 1510.93 184.161C1519.57 184.161 1527.43 186.005 1534.5 189.694C1541.54 193.383 1547.19 198.96 1551.43 206.447C1555.66 213.935 1557.78 223.333 1557.78 234.706V327.257H1522.23V237.253C1522.23 229.503 1520.25 223.684 1516.32 219.797C1512.37 215.911 1506.81 213.979 1499.67 213.979C1491.58 213.979 1485.33 216.592 1480.91 221.796C1476.48 226.999 1474.27 234.421 1474.27 244.082V327.279H1438.73V237.275C1438.73 229.525 1436.74 223.706 1432.81 219.819C1428.87 215.933 1423.3 214.001 1416.17 214.001C1408.07 214.001 1401.83 216.614 1397.4 221.817C1392.97 227.021 1390.77 234.443 1390.77 244.104V327.301H1355.22L1355.18 327.213Z" fill="#1C2026"/>
<path d="M1645.78 331.185C1631.86 331.185 1619.59 328.199 1608.97 322.248C1598.34 316.276 1590.07 307.866 1584.14 296.975C1578.22 286.084 1575.26 273.261 1575.26 258.506V255.103C1575.26 240.347 1578.16 227.524 1584.01 216.633C1589.83 205.743 1598.01 197.333 1608.55 191.36C1619.09 185.41 1631.31 182.424 1645.23 182.424C1659.15 182.424 1670.91 185.498 1681.06 191.646C1691.21 197.794 1699.13 206.313 1704.76 217.204C1710.41 228.095 1713.22 240.721 1713.22 255.103V267.311H1611.37C1611.74 276.972 1615.32 284.811 1622.08 290.871C1628.85 296.931 1637.14 299.961 1646.91 299.961C1656.69 299.961 1664.22 297.788 1668.93 293.44C1673.62 289.093 1677.2 284.262 1679.64 278.97L1708.71 294.296C1706.07 299.215 1702.27 304.573 1697.27 310.347C1692.28 316.122 1685.66 321.04 1677.37 325.103C1669.1 329.165 1658.56 331.207 1645.78 331.207V331.185ZM1611.65 240.611H1677.11C1676.35 232.465 1673.12 225.943 1667.38 221.025C1661.64 216.106 1654.16 213.647 1644.95 213.647C1635.74 213.647 1627.73 216.106 1622.1 221.025C1616.45 225.943 1612.98 232.487 1611.67 240.611H1611.65Z" fill="#1C2026"/>
<path d="M1730.7 327.213V186.4H1765.68V201.726H1770.76C1773.2 197.006 1777.24 192.878 1782.89 189.386C1788.54 185.895 1795.96 184.139 1805.17 184.139C1815.14 184.139 1823.13 186.071 1829.15 189.957C1835.17 193.844 1839.78 198.894 1842.98 205.152H1848.07C1851.25 199.092 1855.77 194.085 1861.62 190.111C1867.44 186.137 1875.71 184.161 1886.45 184.161C1895.09 184.161 1902.94 186.005 1910.01 189.694C1917.06 193.383 1922.71 198.96 1926.95 206.447C1931.18 213.935 1933.3 223.333 1933.3 234.706V327.257H1897.75V237.253C1897.75 229.503 1895.77 223.684 1891.84 219.797C1887.89 215.911 1882.33 213.979 1875.19 213.979C1867.1 213.979 1860.85 216.592 1856.43 221.796C1852 226.999 1849.79 234.421 1849.79 244.082V327.279H1814.25V237.275C1814.25 229.525 1812.26 223.706 1808.33 219.819C1804.38 215.933 1798.82 214.001 1791.69 214.001C1783.59 214.001 1777.35 216.614 1772.92 221.817C1768.49 227.021 1766.29 234.443 1766.29 244.104V327.301H1730.74L1730.7 327.213Z" fill="#1C2026"/>
<path d="M2024.13 331.185C2010.21 331.185 1997.71 328.352 1986.6 322.665C1975.5 316.978 1966.75 308.744 1960.35 297.963C1953.96 287.182 1950.75 274.206 1950.75 259.077V254.532C1950.75 239.381 1953.94 226.426 1960.35 215.645C1966.75 204.864 1975.5 196.63 1986.6 190.943C1997.69 185.256 2010.21 182.424 2024.13 182.424C2038.06 182.424 2050.56 185.256 2061.66 190.943C2072.75 196.63 2081.5 204.864 2087.91 215.645C2094.31 226.426 2097.49 239.403 2097.49 254.532V259.077C2097.49 274.227 2094.28 287.182 2087.91 297.963C2081.52 308.744 2072.77 316.978 2061.66 322.665C2050.56 328.352 2038.06 331.185 2024.13 331.185ZM2024.13 299.391C2035.04 299.391 2044.06 295.833 2051.21 288.741C2058.37 281.649 2061.93 271.461 2061.93 258.221V255.388C2061.93 242.148 2058.39 231.96 2051.34 224.867C2044.3 217.775 2035.22 214.218 2024.11 214.218C2013.01 214.218 2004.17 217.775 1997.03 224.867C1989.88 231.96 1986.32 242.148 1986.32 255.388V258.221C1986.32 271.461 1989.88 281.649 1997.03 288.741C2004.19 295.833 2013.2 299.391 2024.11 299.391H2024.13Z" fill="#1C2026"/>
<path d="M2116.1 327.211V186.399H2151.08V202.296H2156.16C2158.24 196.609 2161.66 192.459 2166.46 189.802C2171.26 187.146 2176.85 185.828 2183.24 185.828H2200.18V217.622H2182.68C2173.64 217.622 2166.22 220.038 2160.4 224.868C2154.57 229.699 2151.65 237.12 2151.65 247.155V327.211H2116.1Z" fill="#1C2026"/>
<path d="M2228.95 383.994V352.771H2305.13C2310.38 352.771 2313.02 349.939 2313.02 344.252V308.769H2307.94C2306.43 311.996 2304.08 315.202 2300.89 318.43C2297.69 321.658 2293.36 324.292 2287.91 326.378C2282.45 328.464 2275.49 329.496 2267.03 329.496C2256.12 329.496 2246.56 326.993 2238.4 321.965C2230.22 316.959 2223.87 309.998 2219.35 301.106C2214.84 292.213 2212.59 281.981 2212.59 270.453V186.4H2248.13V267.599C2248.13 278.204 2250.71 286.153 2255.9 291.444C2261.07 296.736 2268.45 299.393 2278.05 299.393C2288.96 299.393 2297.42 295.748 2303.45 288.458C2309.47 281.168 2312.48 271.002 2312.48 257.937V186.4H2348.02V352.2C2348.02 361.861 2345.21 369.569 2339.56 375.343C2333.91 381.118 2326.38 383.994 2317 383.994H2228.97H2228.95Z" fill="#1C2026"/>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,9 @@
{
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "react-jsx",
"types": ["chrome"]
},
"exclude": ["**/*.test.ts"]
}

View file

@ -0,0 +1,193 @@
/**
* API service for supermemory browser extension
*/
import { API_ENDPOINTS } from "./constants"
import { bearerToken, defaultProject, userData } from "./storage"
import { buildSearchMemoriesBody } from "./search-request"
import {
AuthenticationError,
type MemoryPayload,
type Project,
type ProjectsResponse,
SupermemoryAPIError,
} from "./types"
/**
* Get bearer token from storage
*/
async function getBearerToken(): Promise<string> {
const token = await bearerToken.getValue()
if (!token) {
throw new AuthenticationError("Bearer token not found")
}
return token
}
/**
* Make authenticated API request
*/
async function makeAuthenticatedRequest<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
const token = await getBearerToken()
const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, {
...options,
credentials: "omit",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
})
if (!response.ok) {
if (response.status === 401) {
throw new AuthenticationError("Invalid or expired token")
}
throw new SupermemoryAPIError(
`API request failed: ${response.statusText}`,
response.status,
)
}
return response.json()
}
/**
* Fetch all projects from API
*/
export async function fetchProjects(): Promise<Project[]> {
try {
const response =
await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects")
return response.projects
} catch (error) {
console.error("Failed to fetch projects:", error)
throw error
}
}
/**
* Get default project from storage
*/
export async function getDefaultProject(): Promise<Project | null> {
try {
const defaultProjectValue = await defaultProject.getValue()
return defaultProjectValue || null
} catch (error) {
console.error("Failed to get default project:", error)
return null
}
}
/**
* Set default project in storage
*/
export async function setDefaultProject(project: Project): Promise<void> {
try {
await defaultProject.setValue(project)
} catch (error) {
console.error("Failed to set default project:", error)
throw error
}
}
/**
* Validate if current bearer token is still valid
*/
export async function validateAuthToken(): Promise<boolean> {
try {
await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects")
return true
} catch (error) {
if (error instanceof AuthenticationError) {
return false
}
console.error("Failed to validate auth token:", error)
return true
}
}
/**
* Get user data from storage
*/
export async function getUserData(): Promise<{
email?: string
name?: string
} | null> {
try {
return (await userData.getValue()) || null
} catch (error) {
console.error("Failed to get user data:", error)
return null
}
}
/**
* Save memory to Supermemory API
*/
export async function saveMemory(payload: MemoryPayload): Promise<unknown> {
try {
const response = await makeAuthenticatedRequest<unknown>("/v3/documents", {
method: "POST",
body: JSON.stringify(payload),
})
return response
} catch (error) {
console.error("Failed to save memory:", error)
throw error
}
}
/**
* Search memories using Supermemory API
*/
export async function searchMemories(
query: string,
containerTag?: string,
): Promise<unknown> {
try {
const response = await makeAuthenticatedRequest<unknown>("/v4/search", {
method: "POST",
body: JSON.stringify(buildSearchMemoriesBody(query, containerTag)),
})
return response
} catch (error) {
console.error("Failed to search memories:", error)
throw error
}
}
/**
* Save tweet to Supermemory API (specific for Twitter imports)
*/
export async function saveAllTweets(
documents: MemoryPayload[],
): Promise<unknown> {
try {
const response = await makeAuthenticatedRequest<unknown>(
"/v3/documents/batch",
{
method: "POST",
body: JSON.stringify({
documents,
metadata: {
sm_source: "consumer",
sm_internal_group_id: "twitter_bookmarks",
},
}),
},
)
return response
} catch (error) {
if (error instanceof SupermemoryAPIError && error.statusCode === 409) {
// Skip if already exists (409 Conflict)
return
}
throw error
}
}

View file

@ -0,0 +1,100 @@
/**
* API Endpoints
*/
export const API_ENDPOINTS = {
SUPERMEMORY_API: import.meta.env.PROD
? "https://api.supermemory.ai"
: "http://localhost:8787",
SUPERMEMORY_WEB: import.meta.env.PROD
? "https://app.supermemory.ai"
: "http://localhost:3000",
} as const
/**
* DOM Element IDs
*/
export const ELEMENT_IDS = {
TWITTER_IMPORT_BUTTON: "sm-twitter-import-button",
TWITTER_ONBOARDING_TOAST: "sm-twitter-onboarding-toast",
TWITTER_IMPORT_PROGRESS_TOAST: "sm-twitter-import-progress-toast",
SUPERMEMORY_TOAST: "sm-toast",
SUPERMEMORY_SAVE_BUTTON: "sm-save-button",
SAVE_TWEET_ELEMENT: "sm-save-tweet-element",
CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element",
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
PROJECT_SELECTION_MODAL: "sm-project-selection-modal",
} as const
/**
* Storage Keys for local
*/
export const STORAGE_KEYS = {
TWITTER_BOOKMARKS_ONBOARDING_SEEN: "sm_twitter_bookmarks_onboarding_seen",
TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL:
"sm_twitter_bookmarks_import_intent_until",
} as const
/**
* UI Configuration
*/
export const UI_CONFIG = {
BUTTON_SHOW_DELAY: 2000, // milliseconds
TOAST_DURATION: 3000, // milliseconds
ONBOARDING_TOAST_DURATION: 6000, // milliseconds (6 seconds for progress bar)
IMPORT_INTENT_TTL: 2 * 60 * 1000, // 2 minutes TTL for import intent
RATE_LIMIT_BASE_WAIT: 60000, // 1 minute
PAGINATION_DELAY: 1000, // 1 second between requests
AUTO_SEARCH_DEBOUNCE_DELAY: 1500, // milliseconds to wait after user stops typing
OBSERVER_THROTTLE_DELAY: 300, // milliseconds between observer callback executions
ROUTE_CHECK_INTERVAL: 2000, // milliseconds between route change checks
API_REQUEST_TIMEOUT: 10000, // milliseconds for API request timeout
} as const
/**
* Supported Domains
*/
export const DOMAINS = {
TWITTER: ["x.com", "twitter.com"],
CHATGPT: ["chatgpt.com", "chat.openai.com"],
CLAUDE: ["claude.ai"],
GROK: ["grok.com", "x.ai"],
T3: ["t3.chat"],
SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"],
} as const
/**
* Container Tags
*/
export const CONTAINER_TAGS = {
TWITTER_BOOKMARKS: "sm_project_twitter_bookmarks",
DEFAULT_PROJECT: "sm_project_default",
} as const
/**
* Message Types for extension communication
*/
export const MESSAGE_TYPES = {
SAVE_MEMORY: "sm-save-memory",
SHOW_TOAST: "sm-show-toast",
BATCH_IMPORT_ALL: "sm-batch-import-all",
IMPORT_UPDATE: "sm-import-update",
IMPORT_DONE: "sm-import-done",
GET_RELATED_MEMORIES: "sm-get-related-memories",
CAPTURE_PROMPT: "sm-capture-prompt",
FETCH_PROJECTS: "sm-fetch-projects",
TWITTER_IMPORT_OPEN_MODAL: "sm-twitter-import-open-modal",
} as const
export const POSTHOG_EVENT_KEY = {
TWITTER_IMPORT_STARTED: "twitter_import_started",
SAVE_MEMORY_ATTEMPTED: "save_memory_attempted",
SAVE_MEMORY_ATTEMPT_FAILED: "save_memory_attempt_failed",
SOURCE: "extension",
T3_CHAT_MEMORIES_SEARCHED: "t3_chat_memories_searched",
T3_CHAT_MEMORIES_AUTO_SEARCHED: "t3_chat_memories_auto_searched",
CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched",
CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched",
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched",
} as const

View file

@ -0,0 +1,93 @@
/**
* Memory Popup Utilities
* Standardized popup positioning and styling for memory display across platforms
*/
export interface MemoryPopupConfig {
memoriesData: string
onClose: () => void
onRemove?: () => void
}
export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
const popup = document.createElement("div")
popup.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
padding: 0;
border-radius: 12px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
max-width: 500px;
max-height: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 999999;
display: none;
overflow: hidden;
`
const header = document.createElement("div")
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
`
header.innerHTML = `
<span style="font-size: 11px; font-weight: 600; letter-spacing: 0.5px;">INCLUDED MEMORIES</span>
<div style="display: flex; gap: 4px;">
${config.onRemove ? '<button id="remove-memories-btn" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;" title="Remove memories">✕</button>' : ""}
<button id="close-popup-btn" style="background: none; border: none; color: white; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;"></button>
</div>
`
const content = document.createElement("div")
content.style.cssText = `
padding: 8px;
max-height: 300px;
overflow-y: auto;
line-height: 1.4;
`
content.textContent = config.memoriesData
const closeBtn = header.querySelector("#close-popup-btn")
closeBtn?.addEventListener("click", config.onClose)
const removeBtn = header.querySelector("#remove-memories-btn")
if (removeBtn && config.onRemove) {
removeBtn.addEventListener("click", config.onRemove)
}
popup.appendChild(header)
popup.appendChild(content)
return popup
}
export function showMemoryPopup(popup: HTMLElement): void {
popup.style.display = "block"
setTimeout(() => {
if (popup.style.display === "block") {
hideMemoryPopup(popup)
}
}, 10000)
}
export function hideMemoryPopup(popup: HTMLElement): void {
popup.style.display = "none"
}
export function toggleMemoryPopup(popup: HTMLElement): void {
if (popup.style.display === "none" || popup.style.display === "") {
showMemoryPopup(popup)
} else {
hideMemoryPopup(popup)
}
}

View file

@ -0,0 +1,76 @@
import { PostHog } from "posthog-js/dist/module.no-external"
import { userData } from "./storage"
export async function identifyUser(posthog: PostHog): Promise<void> {
const storedUserData = await userData.getValue()
if (storedUserData?.userId) {
posthog.identify(storedUserData.userId, {
email: storedUserData.email,
name: storedUserData.name,
userId: storedUserData.userId,
})
}
}
let posthogInstance: PostHog | null = null
let initializationPromise: Promise<PostHog> | null = null
export const POSTHOG_CONFIG = {
api_host: "https://api.supermemory.ai/orange",
person_profiles: "identified_only",
disable_external_dependency_loading: true,
persistence: "localStorage",
capture_pageview: false,
autocapture: false,
} as const
export async function getPostHogInstance(): Promise<PostHog> {
if (posthogInstance) {
return posthogInstance
}
if (initializationPromise) {
return initializationPromise
}
initializationPromise = initializePostHog()
return initializationPromise
}
async function initializePostHog(): Promise<PostHog> {
try {
const posthog = new PostHog()
if (!import.meta.env.WXT_POSTHOG_API_KEY) {
console.error("PostHog API key not configured")
throw new Error("PostHog API key not configured")
}
posthog.init(
"phc_ShqecfUPQgf16lWu6ZMUzduQvcWzCywrkCz5KHwmWsv",
POSTHOG_CONFIG,
)
await identifyUser(posthog)
posthogInstance = posthog
return posthog
} catch (error) {
console.error("Failed to initialize PostHog:", error)
initializationPromise = null
throw error
}
}
export async function trackEvent(
eventName: string,
properties?: Record<string, unknown>,
): Promise<void> {
try {
const posthog = await getPostHogInstance()
posthog.capture(eventName, properties)
} catch (error) {
console.error(`Failed to track event ${eventName}:`, error)
}
}

View file

@ -0,0 +1,25 @@
/**
* React Query configuration for supermemory browser extension
*/
import { QueryClient } from "@tanstack/react-query"
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes (previously cacheTime)
retry: (failureCount, error) => {
// Don't retry on authentication errors
if (error?.constructor?.name === "AuthenticationError") {
return false
}
return failureCount < 3
},
refetchOnMount: true,
refetchOnWindowFocus: false,
},
mutations: {
retry: 1,
},
},
})

View file

@ -0,0 +1,76 @@
/**
* React Query hooks for supermemory API
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
fetchProjects,
getDefaultProject,
getUserData,
saveMemory,
searchMemories,
setDefaultProject,
} from "./api"
import type { MemoryPayload } from "./types"
// Query Keys
export const queryKeys = {
projects: ["projects"] as const,
defaultProject: ["defaultProject"] as const,
userData: ["userData"] as const,
}
// Projects Query
export function useProjects(options?: { enabled?: boolean }) {
return useQuery({
queryKey: queryKeys.projects,
queryFn: fetchProjects,
staleTime: 5 * 60 * 1000, // 5 minutes
enabled: options?.enabled ?? true,
})
}
// Default Project Query
export function useDefaultProject(options?: { enabled?: boolean }) {
return useQuery({
queryKey: queryKeys.defaultProject,
queryFn: getDefaultProject,
staleTime: 2 * 60 * 1000, // 2 minutes
enabled: options?.enabled ?? true,
})
}
// User Data Query
export function useUserData(options?: { enabled?: boolean }) {
return useQuery({
queryKey: queryKeys.userData,
queryFn: getUserData,
staleTime: 5 * 60 * 1000, // 5 minutes
enabled: options?.enabled ?? true,
})
}
// Set Default Project Mutation
export function useSetDefaultProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: setDefaultProject,
onSuccess: (_, project) => {
queryClient.setQueryData(queryKeys.defaultProject, project)
},
})
}
// Save Memory Mutation
export function useSaveMemory() {
return useMutation({
mutationFn: (payload: MemoryPayload) => saveMemory(payload),
})
}
// Search Memories Mutation
export function useSearchMemories() {
return useMutation({
mutationFn: (query: string) => searchMemories(query),
})
}

View file

@ -0,0 +1,117 @@
/**
* Route Detection Utilities
* Shared logic for detecting route changes across different AI chat platforms
*/
import { UI_CONFIG } from "./constants"
export interface RouteDetectionConfig {
platform: string
selectors: string[]
reinitCallback: () => void
checkInterval?: number
observerThrottleDelay?: number
}
export interface RouteDetectionCleanup {
observer: MutationObserver | null
urlCheckInterval: NodeJS.Timeout | null
observerThrottle: NodeJS.Timeout | null
}
export function createRouteDetection(
config: RouteDetectionConfig,
cleanup: RouteDetectionCleanup,
): void {
if (cleanup.observer) {
cleanup.observer.disconnect()
}
if (cleanup.urlCheckInterval) {
clearInterval(cleanup.urlCheckInterval)
}
if (cleanup.observerThrottle) {
clearTimeout(cleanup.observerThrottle)
cleanup.observerThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log(`${config.platform} route changed, re-initializing`)
setTimeout(config.reinitCallback, 1000)
}
}
cleanup.urlCheckInterval = setInterval(
checkForRouteChange,
config.checkInterval || UI_CONFIG.ROUTE_CHECK_INTERVAL,
)
cleanup.observer = new MutationObserver((mutations) => {
if (cleanup.observerThrottle) {
return
}
let shouldRecheck = false
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
for (const selector of config.selectors) {
if (
element.querySelector?.(selector) ||
element.matches?.(selector)
) {
shouldRecheck = true
break
}
}
}
})
}
})
if (shouldRecheck) {
cleanup.observerThrottle = setTimeout(() => {
try {
cleanup.observerThrottle = null
config.reinitCallback()
} catch (error) {
console.error(`Error in ${config.platform} observer callback:`, error)
}
}, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY)
}
})
try {
cleanup.observer.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error(`Failed to set up ${config.platform} route observer:`, error)
if (cleanup.urlCheckInterval) {
clearInterval(cleanup.urlCheckInterval)
}
cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
export function cleanupRouteDetection(cleanup: RouteDetectionCleanup): void {
if (cleanup.observer) {
cleanup.observer.disconnect()
cleanup.observer = null
}
if (cleanup.urlCheckInterval) {
clearInterval(cleanup.urlCheckInterval)
cleanup.urlCheckInterval = null
}
if (cleanup.observerThrottle) {
clearTimeout(cleanup.observerThrottle)
cleanup.observerThrottle = null
}
}

View file

@ -0,0 +1,19 @@
import { describe, expect, it } from "bun:test"
import { buildSearchMemoriesBody } from "./search-request"
describe("buildSearchMemoriesBody", () => {
it("builds the default related-memory search body", () => {
expect(buildSearchMemoriesBody("deploy notes")).toEqual({
q: "deploy notes",
include: { relatedMemories: true },
})
})
it("includes the container tag when provided", () => {
expect(buildSearchMemoriesBody("deploy notes", "sm_project_docs")).toEqual({
q: "deploy notes",
include: { relatedMemories: true },
containerTag: "sm_project_docs",
})
})
})

View file

@ -0,0 +1,14 @@
export function buildSearchMemoriesBody(
query: string,
containerTag?: string,
): {
q: string
include: { relatedMemories: boolean }
containerTag?: string
} {
return {
q: query,
include: { relatedMemories: true },
...(containerTag ? { containerTag } : {}),
}
}

View file

@ -0,0 +1,120 @@
/**
* Centralized storage layer using WXT's built-in storage API
*/
import { storage } from "#imports"
import type { Project } from "./types"
/**
* User authentication and profile data
*/
export interface UserData {
userId?: string
email?: string
name?: string
}
/**
* Twitter authentication tokens for API requests
*/
export interface TwitterAuthTokens {
cookie: string
csrf: string
auth: string
}
/**
* Local Storage Items (persistent across sessions)
*/
export const bearerToken = storage.defineItem<string>("local:bearer-token")
export const userData = storage.defineItem<UserData>("local:user-data")
export const defaultProject = storage.defineItem<Project>(
"local:sm-default-project",
)
export const autoSearchEnabled = storage.defineItem<boolean>(
"local:sm-auto-search-enabled",
{
fallback: false,
},
)
export const autoCapturePromptsEnabled = storage.defineItem<boolean>(
"local:sm-auto-capture-prompts-enabled",
{
fallback: false,
},
)
/**
* Session Storage Items (cleared when browser closes)
*/
export const tokensLogged = storage.defineItem<boolean>(
"session:tokens-logged",
{
fallback: false,
},
)
export const twitterCookie = storage.defineItem<string>(
"session:twitter-cookie",
)
export const twitterCsrf = storage.defineItem<string>("session:twitter-csrf")
export const twitterAuthToken = storage.defineItem<string>(
"session:twitter-auth-token",
)
/**
* Helper function to get Twitter authentication tokens
* @returns Promise resolving to tokens or null if not available
*/
export async function getTwitterTokens(): Promise<TwitterAuthTokens | null> {
const [cookie, csrf, auth] = await Promise.all([
twitterCookie.getValue(),
twitterCsrf.getValue(),
twitterAuthToken.getValue(),
])
if (!cookie || !csrf || !auth) {
return null
}
return {
cookie,
csrf,
auth,
}
}
/**
* Helper function to set Twitter authentication tokens
* @param tokens - Twitter authentication tokens to store
*/
export async function setTwitterTokens(
tokens: TwitterAuthTokens,
): Promise<void> {
await Promise.all([
twitterCookie.setValue(tokens.cookie),
twitterCsrf.setValue(tokens.csrf),
twitterAuthToken.setValue(tokens.auth),
])
}
/**
* Helper function to check if tokens have been logged (for one-time logging)
* @returns Promise resolving to boolean indicating if tokens were previously logged
*/
export async function getTokensLogged(): Promise<boolean> {
return (await tokensLogged.getValue()) ?? false
}
/**
* Helper function to mark tokens as logged
*/
export async function setTokensLogged(): Promise<void> {
await tokensLogged.setValue(true)
}

View file

@ -0,0 +1,88 @@
/**
* Twitter Authentication Module
* Handles token capture and storage for Twitter API access
*/
import {
getTokensLogged,
setTokensLogged,
setTwitterTokens,
type TwitterAuthTokens,
} from "./storage"
/**
* Captures Twitter authentication tokens from web request headers
* @param details - Web request details containing headers
* @returns True if tokens were captured, false otherwise
*/
export async function captureTwitterTokens(
details: chrome.webRequest.WebRequestDetails & {
requestHeaders?: chrome.webRequest.HttpHeader[]
},
): Promise<boolean> {
if (!(details.url.includes("x.com") || details.url.includes("twitter.com"))) {
return false
}
let authHeader: chrome.webRequest.HttpHeader | undefined
let cookieHeader: chrome.webRequest.HttpHeader | undefined
let csrfHeader: chrome.webRequest.HttpHeader | undefined
if (details.requestHeaders) {
for (const header of details.requestHeaders) {
if (!header.name) continue
const name = header.name.toLowerCase()
switch (name) {
case "authorization":
authHeader = header
break
case "cookie":
cookieHeader = header
break
case "x-csrf-token":
csrfHeader = header
break
}
if (authHeader && cookieHeader && csrfHeader) break
}
}
if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) {
const tokensAlreadyLogged = await getTokensLogged()
if (!tokensAlreadyLogged) {
console.log("Twitter auth tokens captured successfully")
await setTokensLogged()
}
await setTwitterTokens({
cookie: cookieHeader.value,
csrf: csrfHeader.value,
auth: authHeader.value,
})
return true
}
return false
}
/**
* Creates HTTP headers for Twitter API requests using stored tokens
* @param tokens - Twitter authentication tokens
* @returns Headers object ready for fetch requests
*/
export function createTwitterAPIHeaders(tokens: TwitterAuthTokens): Headers {
const headers = new Headers()
headers.append("Cookie", tokens.cookie)
headers.append("X-Csrf-Token", tokens.csrf)
headers.append("Authorization", tokens.auth)
headers.append("Content-Type", "application/json")
headers.append(
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
)
headers.append("Accept", "*/*")
headers.append("Accept-Language", "en-US,en;q=0.9")
return headers
}

View file

@ -0,0 +1,218 @@
/**
* Twitter Bookmarks Import Module
* Handles the import process for Twitter bookmarks
*/
import { saveAllTweets } from "./api"
import type { MemoryPayload } from "./types"
import { createTwitterAPIHeaders } from "./twitter-auth"
import { getTwitterTokens } from "./storage"
import {
BOOKMARKS_URL,
BOOKMARK_COLLECTION_URL,
buildRequestVariables,
buildBookmarkCollectionVariables,
extractNextCursor,
getAllTweets,
type TwitterAPIResponse,
} from "./twitter-utils"
export type ImportProgressCallback = (message: string) => Promise<void>
export type ImportCompleteCallback = (totalImported: number) => Promise<void>
export interface TwitterImportConfig {
isFolderImport?: boolean
bookmarkCollectionId?: string
selectedProject?: {
id: string
name: string
containerTag: string
}
onProgress: ImportProgressCallback
onComplete: ImportCompleteCallback
onError: (error: Error) => Promise<void>
}
/**
* Rate limiting configuration
*/
class RateLimiter {
private waitTime = 60000 // Start with 1 minute
async handleRateLimit(onProgress: ImportProgressCallback): Promise<void> {
const waitTimeInSeconds = this.waitTime / 1000
await onProgress(
`Rate limit reached. Waiting for ${waitTimeInSeconds} seconds before retrying...`,
)
await new Promise((resolve) => setTimeout(resolve, this.waitTime))
this.waitTime *= 2 // Exponential backoff
}
reset(): void {
this.waitTime = 60000
}
}
/**
* Main class for handling Twitter bookmarks import
*/
export class TwitterImporter {
private importInProgress = false
private rateLimiter = new RateLimiter()
constructor(private config: TwitterImportConfig) {}
/**
* Starts the import process for all Twitter bookmarks
* @returns Promise that resolves when import is complete
*/
async startImport(): Promise<void> {
if (this.importInProgress) {
throw new Error("Import already in progress")
}
this.importInProgress = true
const uniqueGroupId = crypto.randomUUID()
try {
await this.batchImportAll("", 0, uniqueGroupId)
this.rateLimiter.reset()
} catch (error) {
await this.config.onError(error as Error)
} finally {
this.importInProgress = false
}
}
/**
* Recursive function to import all bookmarks with pagination
* @param cursor - Pagination cursor for Twitter API
* @param totalImported - Number of tweets imported so far
*/
private async batchImportAll(
cursor = "",
totalImported = 0,
uniqueGroupId = "twitter_bookmarks",
): Promise<void> {
try {
// Use a local variable to track imported count
let importedCount = totalImported
// Get authentication tokens
const tokens = await getTwitterTokens()
if (!tokens) {
await this.config.onProgress(
"Please visit Twitter/X first to capture authentication tokens",
)
return
}
// Create headers for API request
const headers = createTwitterAPIHeaders(tokens)
// Build API request with pagination
const variables =
this.config.isFolderImport && this.config.bookmarkCollectionId
? buildBookmarkCollectionVariables(this.config.bookmarkCollectionId)
: buildRequestVariables(cursor)
const urlWithCursor = cursor
? `${
this.config.isFolderImport && this.config.bookmarkCollectionId
? `${BOOKMARK_COLLECTION_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
: BOOKMARKS_URL
}&variables=${encodeURIComponent(JSON.stringify(variables))}`
: this.config.isFolderImport && this.config.bookmarkCollectionId
? `${BOOKMARK_COLLECTION_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
: `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
const response = await fetch(urlWithCursor, {
method: "GET",
headers,
redirect: "follow",
})
if (!response.ok) {
const errorText = await response.text()
console.error(`Twitter API Error ${response.status}:`, errorText)
if (response.status === 429) {
await this.rateLimiter.handleRateLimit(this.config.onProgress)
return this.batchImportAll(cursor, totalImported, uniqueGroupId)
}
throw new Error(
`Failed to fetch data: ${response.status} - ${errorText}`,
)
}
const data: TwitterAPIResponse = await response.json()
const tweets = getAllTweets(data)
const documents: MemoryPayload[] = []
// Convert tweets to MemoryPayload
for (const tweet of tweets) {
try {
const metadata = {
sm_source: "consumer",
tweet_id: tweet.id_str,
author: tweet.user.screen_name,
created_at: tweet.created_at,
likes: tweet.favorite_count,
retweets: tweet.retweet_count || 0,
sm_internal_group_id: uniqueGroupId,
}
const containerTag =
this.config.selectedProject?.containerTag ||
"sm_project_twitter_bookmarks"
documents.push({
containerTags: [containerTag],
content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
metadata,
customId: tweet.id_str,
})
importedCount++
await this.config.onProgress(
`Imported ${importedCount} tweets, so far...`,
)
} catch (error) {
console.error("Error importing tweet:", error)
}
}
try {
if (documents.length > 0) {
await saveAllTweets(documents)
}
console.log("Tweets saved")
console.log("Documents:", documents)
} catch (error) {
console.error("Error saving tweets batch:", error)
await this.config.onError(error as Error)
return
}
// Handle pagination
const instructions =
data.data?.bookmark_timeline_v2?.timeline?.instructions ||
data.data?.bookmark_collection_timeline?.timeline?.instructions ||
[]
const nextCursor = extractNextCursor(instructions)
console.log("Next cursor:", nextCursor)
console.log("Tweets length:", tweets.length)
if (nextCursor && tweets.length > 0 && !this.config.isFolderImport) {
await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
await this.batchImportAll(nextCursor, importedCount, uniqueGroupId)
} else {
await this.config.onComplete(importedCount)
}
} catch (error) {
console.error("Batch import error:", error)
await this.config.onError(error as Error)
}
}
}

View file

@ -0,0 +1,442 @@
// Twitter API data structures and transformation utilities
interface TwitterAPITweet {
__typename?: string
legacy: {
lang?: string
favorite_count: number
created_at: string
display_text_range?: [number, number]
entities?: {
hashtags?: Array<{ indices: [number, number]; text: string }>
urls?: Array<{
display_url: string
expanded_url: string
indices: [number, number]
url: string
}>
user_mentions?: Array<{
id_str: string
indices: [number, number]
name: string
screen_name: string
}>
symbols?: Array<{ indices: [number, number]; text: string }>
media?: MediaEntity[]
}
id_str: string
full_text: string
reply_count?: number
retweet_count?: number
quote_count?: number
}
core?: {
user_results?: {
result?: {
legacy?: {
id_str: string
name: string
profile_image_url_https: string
screen_name: string
verified: boolean
}
is_blue_verified?: boolean
}
}
}
}
interface MediaEntity {
type: string
media_url_https: string
sizes?: {
large?: {
w: number
h: number
}
}
video_info?: {
variants?: Array<{
url: string
}>
duration_millis?: number
}
}
export interface Tweet {
__typename?: string
lang?: string
favorite_count: number
created_at: string
display_text_range?: [number, number]
entities: {
hashtags: Array<{
indices: [number, number]
text: string
}>
urls?: Array<{
display_url: string
expanded_url: string
indices: [number, number]
url: string
}>
user_mentions: Array<{
id_str: string
indices: [number, number]
name: string
screen_name: string
}>
symbols: Array<{
indices: [number, number]
text: string
}>
}
id_str: string
text: string
user: {
id_str: string
name: string
profile_image_url_https: string
screen_name: string
verified: boolean
is_blue_verified?: boolean
}
conversation_count: number
photos?: Array<{
url: string
width: number
height: number
}>
videos?: Array<{
url: string
thumbnail_url: string
duration: number
}>
retweet_count?: number
quote_count?: number
reply_count?: number
}
export interface TwitterAPIResponse {
data: {
bookmark_timeline_v2?: {
timeline: {
instructions: Array<{
type: string
entries?: Array<{
entryId: string
sortIndex: string
content: Record<string, unknown>
}>
}>
}
}
bookmark_collection_timeline?: {
timeline: {
instructions: Array<{
type: string
entries?: Array<{
entryId: string
sortIndex: string
content: Record<string, unknown>
}>
}>
}
}
}
}
// Twitter API features configuration
export const TWITTER_API_FEATURES = {
graphql_timeline_v2_bookmark_timeline: true,
responsive_web_graphql_exclude_directive_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
responsive_web_graphql_timeline_navigation_enabled: true,
responsive_web_enhance_cards_enabled: false,
rweb_tipjar_consumption_enabled: true,
responsive_web_twitter_article_notes_tab_enabled: true,
creator_subscriptions_tweet_preview_api_enabled: true,
freedom_of_speech_not_reach_fetch_enabled: true,
standardized_nudges_misinfo: true,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
longform_notetweets_rich_text_read_enabled: true,
longform_notetweets_inline_media_enabled: true,
responsive_web_media_download_video_enabled: false,
responsive_web_text_conversations_enabled: false,
// Missing features that the API is complaining about
creator_subscriptions_quote_tweet_preview_enabled: true,
view_counts_everywhere_api_enabled: true,
c9s_tweet_anatomy_moderator_badge_enabled: true,
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
tweetypie_unmention_optimization_enabled: true,
responsive_web_twitter_article_tweet_consumption_enabled: true,
tweet_awards_web_tipping_enabled: true,
communities_web_enable_tweet_community_results_fetch: true,
responsive_web_edit_tweet_api_enabled: true,
longform_notetweets_consumption_enabled: true,
articles_preview_enabled: true,
rweb_video_timestamps_enabled: true,
verified_phone_label_enabled: true,
}
// Twitter API features configuration for BookmarkFolderTimeline
export const TWITTER_BOOKMARK_FOLDER_FEATURES = {
rweb_video_screen_enabled: false,
payments_enabled: false,
profile_label_improvements_pcf_label_in_post_enabled: true,
responsive_web_profile_redirect_enabled: false,
rweb_tipjar_consumption_enabled: true,
verified_phone_label_enabled: false,
creator_subscriptions_tweet_preview_api_enabled: true,
responsive_web_graphql_timeline_navigation_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
premium_content_api_read_enabled: false,
communities_web_enable_tweet_community_results_fetch: true,
c9s_tweet_anatomy_moderator_badge_enabled: true,
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
responsive_web_grok_analyze_post_followups_enabled: true,
responsive_web_jetfuel_frame: true,
responsive_web_grok_share_attachment_enabled: true,
articles_preview_enabled: true,
responsive_web_edit_tweet_api_enabled: true,
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
view_counts_everywhere_api_enabled: true,
longform_notetweets_consumption_enabled: true,
responsive_web_twitter_article_tweet_consumption_enabled: true,
tweet_awards_web_tipping_enabled: false,
responsive_web_grok_show_grok_translated_post: true,
responsive_web_grok_analysis_button_from_backend: true,
creator_subscriptions_quote_tweet_preview_enabled: false,
freedom_of_speech_not_reach_fetch_enabled: true,
standardized_nudges_misinfo: true,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
longform_notetweets_rich_text_read_enabled: true,
longform_notetweets_inline_media_enabled: true,
responsive_web_grok_image_annotation_enabled: true,
responsive_web_grok_imagine_annotation_enabled: true,
responsive_web_grok_community_note_auto_translation_is_enabled: false,
responsive_web_enhance_cards_enabled: false,
}
export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`
export const BOOKMARK_COLLECTION_URL = `https://x.com/i/api/graphql/I8Y9ni1dqP-ZSpwxqJQ--Q/BookmarkFolderTimeline?features=${encodeURIComponent(JSON.stringify(TWITTER_BOOKMARK_FOLDER_FEATURES))}`
/**
* Transform raw Twitter API response data into standardized Tweet format
*/
export function transformTweetData(
input: Record<string, unknown>,
): Tweet | null {
try {
const content = input.content as {
itemContent?: { tweet_results?: { result?: unknown } }
}
const tweetData = content?.itemContent?.tweet_results?.result
if (!tweetData) {
return null
}
const tweet = tweetData as TwitterAPITweet
if (!tweet.legacy) {
return null
}
// Handle media entities
const media = (tweet.legacy.entities?.media as MediaEntity[]) || []
const photos = media
.filter((m) => m.type === "photo")
.map((m) => ({
url: m.media_url_https,
width: m.sizes?.large?.w || 0,
height: m.sizes?.large?.h || 0,
}))
const videos = media
.filter((m) => m.type === "video")
.map((m) => ({
url: m.video_info?.variants?.[0]?.url || "",
thumbnail_url: m.media_url_https,
duration: m.video_info?.duration_millis || 0,
}))
const transformed: Tweet = {
__typename: tweet.__typename,
lang: tweet.legacy?.lang,
favorite_count: tweet.legacy.favorite_count || 0,
created_at: new Date(tweet.legacy.created_at).toISOString(),
display_text_range: tweet.legacy.display_text_range,
entities: {
hashtags: tweet.legacy.entities?.hashtags || [],
urls: tweet.legacy.entities?.urls || [],
user_mentions: tweet.legacy.entities?.user_mentions || [],
symbols: tweet.legacy.entities?.symbols || [],
},
id_str: tweet.legacy.id_str,
text: tweet.legacy.full_text,
user: {
id_str: tweet.core?.user_results?.result?.legacy?.id_str || "",
name: tweet.core?.user_results?.result?.legacy?.name || "Unknown",
profile_image_url_https:
tweet.core?.user_results?.result?.legacy?.profile_image_url_https ||
"",
screen_name:
tweet.core?.user_results?.result?.legacy?.screen_name || "unknown",
verified: tweet.core?.user_results?.result?.legacy?.verified || false,
is_blue_verified:
tweet.core?.user_results?.result?.is_blue_verified || false,
},
conversation_count: tweet.legacy.reply_count || 0,
retweet_count: tweet.legacy.retweet_count || 0,
quote_count: tweet.legacy.quote_count || 0,
reply_count: tweet.legacy.reply_count || 0,
}
if (photos.length > 0) {
transformed.photos = photos
}
if (videos.length > 0) {
transformed.videos = videos
}
return transformed
} catch (error) {
console.error("Error transforming tweet data:", error)
return null
}
}
/**
* Extract all tweets from Twitter API response
*/
export function getAllTweets(data: TwitterAPIResponse): Tweet[] {
const tweets: Tweet[] = []
try {
const instructions =
data.data?.bookmark_timeline_v2?.timeline?.instructions ||
data.data?.bookmark_collection_timeline?.timeline?.instructions ||
[]
for (const instruction of instructions) {
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
for (const entry of instruction.entries) {
if (entry.entryId.startsWith("tweet-")) {
const tweet = transformTweetData(entry)
if (tweet) {
tweets.push(tweet)
}
}
}
}
}
} catch (error) {
console.error("Error extracting tweets:", error)
}
return tweets
}
/**
* Extract pagination cursor from Twitter API response
*/
export function extractNextCursor(
instructions: Array<Record<string, unknown>>,
): string | null {
try {
for (const instruction of instructions) {
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
const entries = instruction.entries as Array<{
entryId: string
content?: { value?: string }
}>
for (const entry of entries) {
if (entry.entryId.startsWith("cursor-bottom-")) {
return entry.content?.value || null
}
}
}
}
} catch (error) {
console.error("Error extracting cursor:", error)
}
return null
}
/**
* Convert Tweet object to markdown format for storage
*/
export function tweetToMarkdown(tweet: Tweet): string {
const username = tweet.user?.screen_name || "unknown"
const displayName = tweet.user?.name || "Unknown User"
const date = new Date(tweet.created_at).toLocaleDateString()
const time = new Date(tweet.created_at).toLocaleTimeString()
let markdown = `# Tweet by @${username} (${displayName})\n\n`
markdown += `**Date:** ${date} ${time}\n`
markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`
// Add tweet text
markdown += `${tweet.text}\n\n`
// Add media if present
if (tweet.photos && tweet.photos.length > 0) {
markdown += "**Images:**\n"
tweet.photos.forEach((photo, index) => {
markdown += `![Image ${index + 1}](${photo.url})\n`
})
markdown += "\n"
}
if (tweet.videos && tweet.videos.length > 0) {
markdown += "**Videos:**\n"
tweet.videos.forEach((video, index) => {
markdown += `[Video ${index + 1}](${video.url})\n`
})
markdown += "\n"
}
// Add hashtags and mentions
if (tweet.entities.hashtags.length > 0) {
markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n`
}
if (tweet.entities.user_mentions.length > 0) {
markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n`
}
// Add raw data for reference
markdown += `\n---\n<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`
return markdown
}
/**
* Build Twitter API request variables for pagination
*/
export function buildRequestVariables(cursor?: string, count = 100) {
const variables = {
count,
includePromotedContent: false,
}
if (cursor) {
;(variables as Record<string, unknown>).cursor = cursor
}
return variables
}
/**
* Build Twitter API request variables for bookmark collection
*/
export function buildBookmarkCollectionVariables(bookmarkCollectionId: string) {
return {
bookmark_collection_id: bookmarkCollectionId,
includePromotedContent: true,
}
}

View file

@ -0,0 +1,162 @@
/**
* Type definitions for the browser extension
*/
/**
* Toast states for UI feedback
*/
export type ToastState = "loading" | "success" | "error"
/**
* Message types for extension communication
*/
export interface ExtensionMessage {
isFolderImport?: boolean
bookmarkCollectionId?: string
action?: string
type?: string
data?: unknown
state?: ToastState
importedMessage?: string
totalImported?: number
actionSource?: string
selectedProject?: {
id: string
name: string
containerTag: string
}
}
/**
* Memory data structure for saving content
*/
export interface MemoryData {
html?: string
markdown?: string
content?: string
highlightedText?: string
url?: string
ogImage?: string
title?: string
}
/**
* Supermemory API payload for storing memories
*/
export interface MemoryPayload {
containerTags?: string[]
content: string
metadata: {
sm_source: string
[key: string]: unknown
}
customId?: string
}
/**
* Twitter-specific memory metadata
*/
export interface TwitterMemoryMetadata {
sm_source: "twitter_bookmarks"
tweet_id: string
author: string
created_at: string
likes: number
retweets: number
}
/**
* Storage data structure for Chrome storage
*/
export interface StorageData {
bearerToken?: string
twitterAuth?: {
cookie: string
csrf: string
auth: string
}
tokens_logged?: boolean
cookie?: string
csrf?: string
auth?: string
defaultProject?: Project
projectsCache?: {
projects: Project[]
timestamp: number
}
}
/**
* Context menu click info
*/
export interface ContextMenuClickInfo {
menuItemId: string | number
editable?: boolean
frameId?: number
frameUrl?: string
linkUrl?: string
mediaType?: string
pageUrl?: string
parentMenuItemId?: string | number
selectionText?: string
srcUrl?: string
targetElementId?: number
wasChecked?: boolean
}
/**
* API Response types
*/
export interface APIResponse<T = unknown> {
success: boolean
data?: T
error?: string
}
/**
* Error types for better error handling
*/
export class ExtensionError extends Error {
constructor(
message: string,
public code?: string,
public statusCode?: number,
) {
super(message)
this.name = "ExtensionError"
}
}
export class TwitterAPIError extends ExtensionError {
constructor(message: string, statusCode?: number) {
super(message, "TWITTER_API_ERROR", statusCode)
this.name = "TwitterAPIError"
}
}
export class SupermemoryAPIError extends ExtensionError {
constructor(message: string, statusCode?: number) {
super(message, "SUPERMEMORY_API_ERROR", statusCode)
this.name = "SupermemoryAPIError"
}
}
export class AuthenticationError extends ExtensionError {
constructor(message = "Authentication required") {
super(message, "AUTH_ERROR")
this.name = "AuthenticationError"
}
}
export interface Project {
id: string
name: string
containerTag: string
createdAt: string
updatedAt: string
documentCount: number
}
export interface ProjectsResponse {
projects: Project[]
}

View file

@ -0,0 +1,781 @@
/**
* UI Components Module
* Reusable UI components for the browser extension
*/
import { ELEMENT_IDS, UI_CONFIG } from "./constants"
import type { ToastState } from "./types"
/**
* Creates a toast notification element
* @param state - The state of the toast (loading, success, error)
* @returns HTMLElement - The toast element
*/
export function createToast(state: ToastState): HTMLElement {
const toast = document.createElement("div")
toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 2147483647;
background: #ffffff;
border-radius: 9999px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
color: #374151;
min-width: 200px;
max-width: 300px;
animation: slideIn 0.3s ease-out;
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12);
`
// Add keyframe animations and fonts if not already present
if (!document.getElementById("supermemory-toast-styles")) {
const style = document.createElement("style")
style.id = "supermemory-toast-styles"
style.textContent = `
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-Light.ttf")}') format('truetype');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-Regular.ttf")}') format('truetype');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-Medium.ttf")}') format('truetype');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-SemiBold.ttf")}') format('truetype');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('${chrome.runtime.getURL("fonts/SpaceGrotesk-Bold.ttf")}') format('truetype');
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes fadeOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`
document.head.appendChild(style)
}
const icon = document.createElement("div")
icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;"
let textElement: HTMLElement = document.createElement("span")
textElement.style.fontWeight = "500"
// Configure toast based on state
switch (state) {
case "loading":
icon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6V2" stroke="#6366f1" stroke-width="2" stroke-linecap="round"/>
<path d="M12 22V18" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<path d="M20.49 8.51L18.36 6.38" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
<path d="M5.64 17.64L3.51 15.51" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<path d="M22 12H18" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.8"/>
<path d="M6 12H2" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<path d="M20.49 15.49L18.36 17.62" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.9"/>
<path d="M5.64 6.36L3.51 8.49" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
</svg>
`
icon.style.animation = "spin 1s linear infinite"
textElement.textContent = "Adding to Memory..."
break
case "success": {
const iconUrl = browser.runtime.getURL("/icon-16.png")
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
textElement.textContent = "Added to Memory"
break
}
case "error": {
icon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#ef4444"/>
<path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`
const textContainer = document.createElement("div")
textContainer.style.cssText =
"display: flex; flex-direction: column; gap: 2px;"
const mainText = document.createElement("span")
mainText.style.cssText = "font-weight: 500; line-height: 1.2;"
mainText.textContent = "Failed to save memory"
const helperText = document.createElement("span")
helperText.style.cssText =
"font-size: 12px; color: #6b7280; font-weight: 400; line-height: 1.2;"
helperText.textContent = "Make sure you are logged in"
textContainer.appendChild(mainText)
textContainer.appendChild(helperText)
textElement = textContainer
break
}
}
toast.appendChild(icon)
toast.appendChild(textElement)
return toast
}
/**
* Creates the Twitter import button
* @param onClick - Click handler for the button
* @returns HTMLElement - The button element
*/
export function createTwitterImportButton(onClick: () => void): HTMLElement {
const button = document.createElement("div")
button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON
button.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
z-index: 2147483646;
background: #ffffff;
color: black;
border: none;
border-radius: 50px;
padding: 10px 16px 10px 32px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`
const iconUrl = browser.runtime.getURL("/icon-16.png")
button.style.backgroundImage = `url("${iconUrl}")`
button.style.backgroundRepeat = "no-repeat"
button.style.backgroundSize = "20px 20px"
button.style.backgroundPosition = "8px center"
const textSpan = document.createElement("span")
textSpan.id = "sm-import-text"
textSpan.style.cssText = "font-weight: 500; font-size: 12px;"
textSpan.textContent = "Import Bookmarks"
button.appendChild(textSpan)
button.addEventListener("mouseenter", () => {
button.style.opacity = "0.8"
button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)"
})
button.addEventListener("mouseleave", () => {
button.style.opacity = "1"
button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)"
})
button.addEventListener("click", onClick)
return button
}
/**
* Creates a save tweet element button for Twitter/X
* @param onClick - Click handler for the button
* @returns HTMLElement - The save button element
*/
export function createSaveTweetElement(onClick: () => void): HTMLElement {
const iconButton = document.createElement("div")
iconButton.style.cssText = `
display: inline-flex;
align-items: flex-end;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
margin-right: 10px;
margin-bottom: 2px;
z-index: 1000;
`
const iconFileName = "/icon-16.png"
const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
`
iconButton.addEventListener("mouseenter", () => {
iconButton.style.opacity = "1"
})
iconButton.addEventListener("mouseleave", () => {
iconButton.style.opacity = "0.7"
})
iconButton.addEventListener("click", (event) => {
event.stopPropagation()
event.preventDefault()
onClick()
})
return iconButton
}
/**
* Creates a save element button for ChatGPT input bar
* @param onClick - Click handler for the button
* @returns HTMLElement - The save button element
*/
export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
const iconButton = document.createElement("div")
iconButton.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
width: auto;
height: 24px;
cursor: pointer;
transition: opacity 0.2s ease;
border-radius: 50%;
`
// Use appropriate icon based on theme
const iconFileName = "/icon-16.png"
const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 50%;" />
`
iconButton.addEventListener("mouseenter", () => {
iconButton.style.opacity = "0.8"
})
iconButton.addEventListener("mouseleave", () => {
iconButton.style.opacity = "1"
})
iconButton.addEventListener("click", (event) => {
event.stopPropagation()
event.preventDefault()
onClick()
})
return iconButton
}
/**
* Creates a save element button for Claude input bar
* @param onClick - Click handler for the button
* @returns HTMLElement - The save button element
*/
export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
const iconButton = document.createElement("div")
iconButton.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
`
const iconFileName = "/icon-16.png"
const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
`
iconButton.addEventListener("mouseenter", () => {
iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"
iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"
})
iconButton.addEventListener("mouseleave", () => {
iconButton.style.backgroundColor = "transparent"
iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"
})
iconButton.addEventListener("click", (event) => {
event.stopPropagation()
event.preventDefault()
onClick()
})
return iconButton
}
/**
* Creates a save element button for T3.chat input bar
* @param onClick - Click handler for the button
* @returns HTMLElement - The save button element
*/
export function createT3InputBarElement(onClick: () => void): HTMLElement {
const iconButton = document.createElement("div")
iconButton.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
`
const iconFileName = "/icon-16.png"
const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
`
iconButton.addEventListener("mouseenter", () => {
iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"
iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"
})
iconButton.addEventListener("mouseleave", () => {
iconButton.style.backgroundColor = "transparent"
iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"
})
iconButton.addEventListener("click", (event) => {
event.stopPropagation()
event.preventDefault()
onClick()
})
return iconButton
}
/**
* Creates a project selection modal for Twitter folder imports
* @param projects - Array of available projects
* @param onImport - Callback when import is clicked with selected project
* @param onClose - Callback when modal is closed
* @returns HTMLElement - The modal element
*/
export function createProjectSelectionModal(
projects: Array<{ id: string; name: string; containerTag: string }>,
onImport: (project: {
id: string
name: string
containerTag: string
}) => void,
onClose: () => void,
): HTMLElement {
const modal = document.createElement("div")
modal.id = "sm-project-selection-modal"
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483648;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`
const dialog = document.createElement("div")
dialog.style.cssText = `
background: #05070A;
border-radius: 12px;
padding: 24px;
max-width: 400px;
width: 90%;
box-shadow: 0 8px 32px rgba(5, 7, 10, 0.2);
position: relative;
`
const header = document.createElement("div")
header.style.cssText = `
margin-bottom: 20px;
`
const iconUrl = browser.runtime.getURL("/icon-16.png")
header.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
<h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #ffffff; display: flex; align-items: center; gap: 8px;">
<img src="${iconUrl}" width="20" height="20" alt="Supermemory" style="border-radius: 4px;" />
Import to Supermemory
</h3>
<p style="margin: 0; font-size: 14px; font-weight: 400; color: #ffffff; opacity: 0.7;">
The project you want to import your bookmarks to.
</p>
</div>
`
const form = document.createElement("div")
form.style.cssText = `
display: flex;
flex-direction: column;
gap: 16px;
`
const selectContainer = document.createElement("div")
selectContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`
const label = document.createElement("label")
label.style.cssText = `
font-size: 14px;
font-weight: 500;
color: #ffffff;
`
label.textContent = "Select Project to import"
const select = document.createElement("select")
select.id = "project-select"
select.style.cssText = `
padding: 12px 40px 12px 16px;
border: none;
border-radius: 12px;
font-size: 14px;
background: rgba(91, 126, 245, 0.04);
box-shadow: -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset, 2px 2px 1px 0 rgba(0, 0, 0, 0.50) inset;
color: #ffffff;
cursor: pointer;
transition: border-color 0.2s ease;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 16px center;
background-size: 16px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`
select.addEventListener("focus", () => {
select.style.borderColor = "#1A88FF"
})
select.addEventListener("blur", () => {
select.style.borderColor = "#374151"
})
// Add default option
const defaultOption = document.createElement("option")
defaultOption.value = ""
defaultOption.textContent = "Choose a project..."
defaultOption.disabled = true
defaultOption.selected = true
select.appendChild(defaultOption)
// Add project options
projects.forEach((project) => {
const option = document.createElement("option")
option.value = project.id
option.textContent = project.name
option.dataset.containerTag = project.containerTag
select.appendChild(option)
})
const buttonContainer = document.createElement("div")
buttonContainer.style.cssText = `
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 8px;
`
const cancelButton = document.createElement("button")
cancelButton.textContent = "Cancel"
cancelButton.style.cssText = `
padding: 10px 16px;
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 10px;
border: none;
background: #05070A;
`
cancelButton.addEventListener("mouseenter", () => {
cancelButton.style.backgroundColor = "#f9fafb"
cancelButton.style.color = "#05070A"
})
cancelButton.addEventListener("mouseleave", () => {
cancelButton.style.backgroundColor = "#05070A"
cancelButton.style.color = "#ffffff"
})
const importButton = document.createElement("button")
importButton.textContent = "Import"
importButton.style.cssText = `
padding: 10px 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.3);
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
transition: all 0.2s ease;
`
importButton.disabled = true
// Handle project selection
select.addEventListener("change", () => {
const selectedOption = select.options[select.selectedIndex]
if (selectedOption.value) {
importButton.disabled = false
importButton.style.cssText = `
padding: 10px 16px;
border: none;
border-radius: 12px;
background: linear-gradient(203deg, #0FF0D2 -49.88%, #5BD3FB -33.14%, #1E0FF0 81.81%);
box-shadow: 1px 1px 2px 1px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20);
color: #ffffff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
`
} else {
importButton.disabled = true
importButton.style.cssText = `
padding: 10px 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.3);
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
transition: all 0.2s ease;
`
}
})
// Handle import button click
importButton.addEventListener("click", () => {
const selectedOption = select.options[select.selectedIndex]
if (selectedOption.value) {
const selectedProject = {
id: selectedOption.value,
name: selectedOption.textContent,
containerTag: selectedOption.dataset.containerTag || "",
}
onImport(selectedProject)
}
})
// Handle cancel button click
cancelButton.addEventListener("click", onClose)
// Handle overlay click to close
modal.addEventListener("click", (e) => {
if (e.target === modal) {
onClose()
}
})
// Handle escape key
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose()
}
}
document.addEventListener("keydown", handleKeyDown)
// Clean up event listener when modal is removed
const observer = new MutationObserver(() => {
if (!document.contains(modal)) {
document.removeEventListener("keydown", handleKeyDown)
observer.disconnect()
}
})
observer.observe(document.body, { childList: true, subtree: true })
selectContainer.appendChild(label)
selectContainer.appendChild(select)
form.appendChild(selectContainer)
buttonContainer.appendChild(cancelButton)
buttonContainer.appendChild(importButton)
form.appendChild(buttonContainer)
dialog.appendChild(header)
dialog.appendChild(form)
modal.appendChild(dialog)
return modal
}
/**
* Utility functions for DOM manipulation
*/
export const DOMUtils = {
/**
* Check if current page is on specified domains
* @param domains - Array of domain names to check
* @returns boolean
*/
isOnDomain(domains: readonly string[]): boolean {
return domains.includes(window.location.hostname)
},
/**
* Detect if the page is in dark mode based on color-scheme style
* @returns boolean - true if dark mode, false if light mode
*/
isDarkMode(): boolean {
const htmlElement = document.documentElement
const style = htmlElement.getAttribute("style")
return style?.includes("color-scheme: dark") || false
},
/**
* Check if element exists in DOM
* @param id - Element ID to check
* @returns boolean
*/
elementExists(id: string): boolean {
return !!document.getElementById(id)
},
/**
* Remove element from DOM if it exists
* @param id - Element ID to remove
*/
removeElement(id: string): void {
const element = document.getElementById(id)
element?.remove()
},
/**
* Show toast notification with auto-dismiss
* @param state - Toast state
* @param duration - Duration to show toast (default from config)
* @returns The toast element
*/
showToast(
state: ToastState,
duration: number = UI_CONFIG.TOAST_DURATION,
): HTMLElement {
const existingToast = document.getElementById(ELEMENT_IDS.SUPERMEMORY_TOAST)
if ((state === "success" || state === "error") && existingToast) {
const icon = existingToast.querySelector("div")
const text = existingToast.querySelector("span")
if (icon && text) {
if (state === "success") {
const iconUrl = browser.runtime.getURL("/icon-16.png")
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
icon.style.animation = ""
text.textContent = "Added to Memory"
} else if (state === "error") {
icon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#ef4444"/>
<path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`
icon.style.animation = ""
const textContainer = document.createElement("div")
textContainer.style.cssText =
"display: flex; flex-direction: column; gap: 2px;"
const mainText = document.createElement("span")
mainText.style.cssText = "font-weight: 500; line-height: 1.2;"
mainText.textContent = "Failed to save memory"
const helperText = document.createElement("span")
helperText.style.cssText =
"font-size: 12px; color: #6b7280; font-weight: 400; line-height: 1.2;"
helperText.textContent = "Make sure you are logged in"
textContainer.appendChild(mainText)
textContainer.appendChild(helperText)
text.innerHTML = ""
text.appendChild(textContainer)
}
// Auto-dismiss
setTimeout(() => {
if (document.body.contains(existingToast)) {
existingToast.style.animation = "fadeOut 0.3s ease-out"
setTimeout(() => {
if (document.body.contains(existingToast)) {
existingToast.remove()
}
}, 300)
}
}, duration)
return existingToast
}
}
const existingToasts = document.querySelectorAll(
`#${ELEMENT_IDS.SUPERMEMORY_TOAST}`,
)
existingToasts.forEach((toast) => {
toast.remove()
})
const toast = createToast(state)
document.body.appendChild(toast)
// Auto-dismiss for success and error states
if (state === "success" || state === "error") {
setTimeout(() => {
if (document.body.contains(toast)) {
toast.style.animation = "fadeOut 0.3s ease-out"
setTimeout(() => {
if (document.body.contains(toast)) {
toast.remove()
}
}, 300)
}
}, duration)
}
return toast
},
}

View file

@ -0,0 +1,57 @@
import path from "node:path"
import { createRequire } from "node:module"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig, type WxtViteConfig } from "wxt"
const require = createRequire(import.meta.url)
function reactPackageRoot(pkg: "react" | "react-dom"): string {
return path.dirname(require.resolve(`${pkg}/package.json`))
}
// See https://wxt.dev/api/config.html
export default defineConfig({
modules: ["@wxt-dev/module-react"],
vite: () =>
({
plugins: [tailwindcss()],
resolve: {
dedupe: ["react", "react-dom"],
alias: {
react: reactPackageRoot("react"),
"react-dom": reactPackageRoot("react-dom"),
},
},
optimizeDeps: {
include: ["react", "react-dom", "@tanstack/react-query"],
},
}) as WxtViteConfig,
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
version: "6.1.4",
permissions: ["storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",
"*://twitter.com/*",
"*://supermemory.ai/*",
"*://api.supermemory.ai/*",
"*://chatgpt.com/*",
"*://chat.openai.com/*",
"*://grok.com/*",
"*://*.grok.com/*",
"*://x.ai/*",
"*://*.x.ai/*",
"https://*.posthog.com/*",
],
web_accessible_resources: [
{
resources: ["icon-16.png", "fonts/*.ttf"],
matches: ["<all_urls>"],
},
],
},
webExt: {
chromiumArgs: ["--user-data-dir=./.wxt/chrome-data"],
},
})

View file

@ -1,11 +1,15 @@
--- ---
title: "Ingesting context to supermemory" title: "Ingesting context to supermemory"
sidebarTitle: "API" sidebarTitle: "Add context"
description: "Add text, files, and URLs to Supermemory" description: "Add text, files, and URLs to Supermemory"
icon: "plus" icon: "plus"
--- ---
Send any raw content to Supermemory — conversations, documents, files, URLs. We extract the memories automatically. Pass `customId` to identify content and avoid duplicates, and `taskType: "superrag"` if you just need it searchable, not remembered — that's [5x cheaper](#memory-vs-superrag-ingestion) per token. Send any raw content to Supermemory — conversations, documents, files, URLs. We extract the memories automatically.
<Tip>
**Use `customId`** to identify your content (conversation ID, document ID, etc.). This enables updates and prevents duplicates.
</Tip>
## Quick Start ## Quick Start
@ -69,10 +73,6 @@ Send any raw content to Supermemory — conversations, documents, files, URLs. W
{ "id": "abc123", "status": "queued" } { "id": "abc123", "status": "queued" }
``` ```
<Warning>
If an irrecoverable processing error occurs, the document is automatically deleted after 2 minutes.
</Warning>
--- ---
## Updating Content ## Updating Content
@ -155,7 +155,7 @@ Upload PDFs, images, and documents directly.
await client.documents.uploadFile({ await client.documents.uploadFile({
file: fs.createReadStream('document.pdf'), file: fs.createReadStream('document.pdf'),
containerTag: 'user_123' containerTags: 'user_123'
}); });
``` ```
</Tab> </Tab>
@ -164,7 +164,7 @@ Upload PDFs, images, and documents directly.
with open('document.pdf', 'rb') as file: with open('document.pdf', 'rb') as file:
client.documents.upload_file( client.documents.upload_file(
file=file, file=file,
container_tag='user_123' container_tags='user_123'
) )
``` ```
</Tab> </Tab>
@ -173,7 +173,7 @@ Upload PDFs, images, and documents directly.
curl -X POST "https://api.supermemory.ai/v3/documents/file" \ curl -X POST "https://api.supermemory.ai/v3/documents/file" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-F "file=@document.pdf" \ -F "file=@document.pdf" \
-F "containerTag=user_123" -F "containerTags=user_123"
``` ```
</Tab> </Tab>
</Tabs> </Tabs>
@ -202,7 +202,6 @@ Upload PDFs, images, and documents directly.
| `filterByMetadata` | object | Filter which existing memories are used as context during ingestion. See [Filtered Writes](#filtered-writes) | | `filterByMetadata` | object | Filter which existing memories are used as context during ingestion. See [Filtered Writes](#filtered-writes) |
| `entityContext` | string | Context for memory extraction on this container tag. Max 1500 chars. See [Customization](/concepts/customization#entity-context) | | `entityContext` | string | Context for memory extraction on this container tag. Max 1500 chars. See [Customization](/concepts/customization#entity-context) |
| `dreaming` | `"dynamic" \| "instant"` | Processing mode. Default `"dynamic"`. `"instant"` processes each document on its own and bills one extra operation. See [Processing Modes](#processing-modes) | | `dreaming` | `"dynamic" \| "instant"` | Processing mode. Default `"dynamic"`. `"instant"` processes each document on its own and bills one extra operation. See [Processing Modes](#processing-modes) |
| `taskType` | `"memory" \| "superrag"` | Pipeline to run. Default `"memory"`. `"superrag"` skips fact extraction and profile updates, doing only chunk/embed/index — at 5x cheaper per token. See [SuperRAG ingestion](/concepts/super-rag#ingesting-as-pure-superrag-tasktype-superrag) |
<AccordionGroup> <AccordionGroup>
<Accordion title="Parameter Details & Examples"> <Accordion title="Parameter Details & Examples">
@ -281,8 +280,6 @@ Upload PDFs, images, and documents directly.
## Processing Modes ## Processing Modes
### Dreaming: dynamic vs instant
The `dreaming` parameter controls how Supermemory turns a document into memories. The `dreaming` parameter controls how Supermemory turns a document into memories.
- `"dynamic"` (default) — groups related documents together so memories form from coherent, logical units rather than one isolated entry at a time. - `"dynamic"` (default) — groups related documents together so memories form from coherent, logical units rather than one isolated entry at a time.
@ -295,22 +292,6 @@ The `dreaming` parameter controls how Supermemory turns a document into memories
} }
``` ```
### Memory vs SuperRAG ingestion
The `taskType` parameter controls whether that content also feeds the memory pipeline.
- `"memory"` (default) — chunks/embeds for search **and** extracts facts, updates the user's profile, and links into the graph.
- `"superrag"` — chunks/embeds for search only. No fact extraction, no profile updates. Priced at **5x cheaper per token** than `"memory"`.
```json
{
"content": "...",
"taskType": "superrag"
}
```
Use `"superrag"` for reference material you want searchable but that shouldn't shape what Supermemory knows about a user. Full explanation: [SuperRAG → Ingesting as pure SuperRAG](/concepts/super-rag#ingesting-as-pure-superrag-tasktype-superrag).
--- ---
## Filtered Writes ## Filtered Writes
@ -496,7 +477,6 @@ console.log(doc.status); // "queued" | "processing" | "done"
## Next Steps ## Next Steps
- [How to backfill historical data](/ingestion/batch-ingest-historical-data) — Import dated content with the batch API - [Search Memories](/search) — Query your content
- [Search Memories](/recall/search) — Query your content - [User Profiles](/user-profiles) — Get user context
- [User Profiles](/recall/user-profiles) — Get user context
- [Organizing & Filtering](/concepts/filtering) — Container tags and metadata - [Organizing & Filtering](/concepts/filtering) — Container tags and metadata

View file

@ -0,0 +1,278 @@
---
title: "Basic Usage"
description: "Simple examples of adding text content to Supermemory"
---
Learn how to add basic text content to Supermemory with simple, practical examples.
## Add Simple Text
The most basic operation - adding plain text content.
<CodeGroup>
```typescript TypeScript
const response = await client.add({
content: "Artificial intelligence is transforming how we work and live"
});
console.log(response);
// Output: { id: "abc123", status: "queued" }
```
```python Python
response = client.add(
content="Artificial intelligence is transforming how we work and live"
)
print(response)
# Output: {"id": "abc123", "status": "queued"}
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Artificial intelligence is transforming how we work and live"
}'
```
</CodeGroup>
## Add with Container Tags
Group related content using container tags.
<CodeGroup>
```typescript TypeScript
const response = await client.add({
content: "Q4 2024 revenue exceeded projections by 15%",
containerTag: "financial_reports"
});
console.log(response.id);
// Output: xyz789
```
```python Python
response = client.add(
content="Q4 2024 revenue exceeded projections by 15%",
container_tag="financial_reports"
)
print(response['id'])
# Output: xyz789
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Q4 2024 revenue exceeded projections by 15%",
"containerTag": "financial_reports"
}'
# Response: {"id": "xyz789", "status": "queued"}
```
</CodeGroup>
## Add with Metadata
Attach metadata for better search and filtering.
<CodeGroup>
```typescript TypeScript
await client.add({
content: "New onboarding flow reduces drop-off by 30%",
containerTag: "product_updates",
metadata: {
impact: "high",
team: "product"
}
});
```
```python Python
client.add(
content="New onboarding flow reduces drop-off by 30%",
container_tag="product_updates",
metadata={
"impact": "high",
"team": "product"
}
)
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "New onboarding flow reduces drop-off by 30%",
"containerTag": "product_updates",
"metadata": {"impact": "high", "team": "product"}
}'
```
</CodeGroup>
## Add Multiple Documents
Process multiple related documents.
<CodeGroup>
```typescript TypeScript
const notes = [
"API redesign discussion",
"Security audit next month",
"New hire starting Monday"
];
const results = await Promise.all(
notes.map(note =>
client.add({
content: note,
containerTag: "meeting_2024_01_15"
})
)
);
```
```python Python
notes = [
"API redesign discussion",
"Security audit next month",
"New hire starting Monday"
]
for note in notes:
client.add(
content=note,
container_tag="meeting_2024_01_15"
)
```
```bash cURL
# Add each note with separate requests
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "API redesign discussion", "containerTag": "meeting_2024_01_15"}'
```
</CodeGroup>
## Add URLs
Process web pages, YouTube videos, and other URLs automatically.
<CodeGroup>
```typescript TypeScript
// Web page
await client.add({
content: "https://example.com/article",
containerTag: "articles"
});
// YouTube video (auto-transcribed)
await client.add({
content: "https://youtube.com/watch?v=dQw4w9WgXcQ",
containerTag: "videos"
});
// Google Docs
await client.add({
content: "https://docs.google.com/document/d/abc123/edit",
containerTag: "docs"
});
```
```python Python
# Web page
client.add(
content="https://example.com/article",
container_tag="articles"
)
# YouTube video (auto-transcribed)
client.add(
content="https://youtube.com/watch?v=dQw4w9WgXcQ",
container_tag="videos"
)
# Google Docs
client.add(
content="https://docs.google.com/document/d/abc123/edit",
container_tag="docs"
)
```
```bash cURL
# Web page
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "https://example.com/article", "containerTag": "articles"}'
# YouTube video
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "https://youtube.com/watch?v=dQw4w9WgXcQ", "containerTag": "videos"}'
```
</CodeGroup>
## Add Markdown Content
Supermemory preserves markdown formatting.
<CodeGroup>
```typescript TypeScript
const markdown = `
# Project Documentation
## Features
- **Real-time sync**
- **AI search**
- **Enterprise security**
`;
await client.add({
content: markdown,
containerTag: "docs"
});
```
```python Python
markdown = """
# Project Documentation
## Features
- **Real-time sync**
- **AI search**
- **Enterprise security**
"""
client.add(
content=markdown,
container_tag="docs"
)
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "# Project Documentation\n\n## Features\n- **Real-time sync**\n- **AI search**", "containerTag": "docs"}'
```
</CodeGroup>

View file

@ -0,0 +1,195 @@
---
title: "File Upload"
description: "Upload PDFs, images, and other files to Supermemory"
---
Upload files directly to Supermemory for automatic content extraction and processing.
## Upload a PDF
Extract text from PDFs with OCR support.
<CodeGroup>
```typescript TypeScript
const file = fs.createReadStream('document.pdf');
const response = await client.documents.uploadFile({
file: file,
containerTags: 'documents'
});
console.log(response.id);
// Output: pdf_123
```
```python Python
with open('document.pdf', 'rb') as file:
response = client.documents.upload_file(
file=file,
container_tags='documents'
)
print(response['id'])
# Output: pdf_123
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents/file" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-F "file=@document.pdf" \
-F "containerTags=documents"
# Response: {"id": "pdf_123", "status": "processing"}
```
</CodeGroup>
## Upload Images with OCR
Extract text from images.
<CodeGroup>
```typescript TypeScript
const image = fs.createReadStream('screenshot.png');
await client.documents.uploadFile({
file: image,
containerTags: 'images'
});
```
```python Python
with open('screenshot.png', 'rb') as file:
client.documents.upload_file(
file=file,
container_tags='images'
)
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents/file" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-F "file=@screenshot.png" \
-F "containerTags=images"
```
</CodeGroup>
## Browser File Upload
Handle browser file uploads.
<CodeGroup>
```javascript JavaScript
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('containerTags', 'uploads');
const response = await fetch('https://api.supermemory.ai/v3/documents/file', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`
},
body: formData
});
const result = await response.json();
console.log(result.id);
```
```typescript React
function handleUpload(file: File) {
const formData = new FormData();
formData.append('file', file);
formData.append('containerTags', 'uploads');
return fetch('https://api.supermemory.ai/v3/documents/file', {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}` },
body: formData
});
}
```
```bash cURL
# Browser uploads use FormData, same as file upload
curl -X POST "https://api.supermemory.ai/v3/documents/file" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-F "file=@document.pdf" \
-F "containerTags=uploads"
```
</CodeGroup>
## Upload Multiple Files
Batch upload with rate limiting.
<CodeGroup>
```typescript TypeScript
for (const file of files) {
const stream = fs.createReadStream(file);
await client.documents.uploadFile({
file: stream,
containerTags: 'batch'
});
// Rate limit
await new Promise(r => setTimeout(r, 1000));
}
```
```python Python
import time
for file_path in files:
with open(file_path, 'rb') as file:
client.documents.upload_file(
file=file,
container_tags='batch'
)
time.sleep(1) # Rate limit
```
```bash cURL
# Upload each file separately with delays
for file in *.pdf; do
curl -X POST "https://api.supermemory.ai/v3/documents/file" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-F "file=@$file" \
-F "containerTags=batch"
sleep 1 # Rate limit
done
```
</CodeGroup>
## Supported File Types
### Documents
| Format | Extensions | Processing |
|--------|------------|------------|
| PDF | .pdf | Text extraction, OCR for scanned pages |
| Microsoft Word | .doc, .docx | Full text and formatting extraction |
| Plain Text | .txt, .md | Direct text processing |
| CSV | .csv | Structured data extraction |
### Images
| Format | Extensions | Processing |
|--------|------------|------------|
| JPEG | .jpg, .jpeg | OCR text extraction |
| PNG | .png | OCR text extraction |
| GIF | .gif | OCR for static images |
| WebP | .webp | OCR text extraction |
### Size Limits
- **Maximum file size**: 50MB
- **Recommended size**: < 10MB for optimal processing
- **Large files**: May take longer to process

View file

@ -0,0 +1,249 @@
---
title: "Add Memories Overview"
description: "Add content to Supermemory through text, files, or URLs"
sidebarTitle: "Overview"
---
Add any type of content to Supermemory - text, files, URLs, images, videos, and more. Everything is automatically processed into searchable memories that form part of your intelligent knowledge graph.
## Prerequisites
Before adding memories, you need to set up the Supermemory client:
- **Install the SDK** for your language
- **Get your API key** from [Supermemory Console](https://console.supermemory.ai)
- **Initialize the client** with your API key
<CodeGroup>
```bash npm
npm install supermemory
```
```bash pip
pip install supermemory
```
</CodeGroup>
<CodeGroup>
```typescript TypeScript
import Supermemory from 'supermemory';
const client = new Supermemory({
apiKey: process.env.SUPERMEMORY_API_KEY!
});
```
```python Python
from supermemory import Supermemory
import os
client = Supermemory(
api_key=os.environ.get("SUPERMEMORY_API_KEY")
)
```
</CodeGroup>
## Quick Start
<CodeGroup>
```typescript TypeScript
// Add text content
const result = await client.add({
content: "Machine learning enables computers to learn from data",
containerTag: "ai-research",
metadata: { priority: "high" }
});
console.log(result);
// Output: { id: "abc123", status: "queued" }
```
```python Python
# Add text content
result = client.add(
content="Machine learning enables computers to learn from data",
container_tags=["ai-research"],
metadata={"priority": "high"}
)
print(result)
# Output: {"id": "abc123", "status": "queued"}
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Machine learning enables computers to learn from data",
"containerTag": "ai-research",
"metadata": {"priority": "high"}
}'
# Response: {"id": "abc123", "status": "queued"}
```
</CodeGroup>
## Key Concepts
<Note>
**New to Supermemory?** Read [How Supermemory Works](/how-it-works) to understand the knowledge graph architecture and the distinction between documents and memories.
</Note>
### Quick Overview
- **Documents**: Raw content you upload (PDFs, URLs, text)
- **Memories**: Searchable chunks created automatically with relationships
- **Container Tags**: Group related content for better context
- **Metadata**: Additional information for filtering
### Content Sources
Add content through three methods:
1. **Direct Text**: Send text content directly via API
2. **File Upload**: Upload PDFs, images, videos for extraction
3. **URL Processing**: Automatic extraction from web pages and platforms
## Endpoints
<Warning>
Remember, these endpoints add documents. Memories are inferred by Supermemory.
</Warning>
### Add Content
`POST /v3/documents`
Add text content, URLs, or any supported format.
<CodeGroup>
```typescript TypeScript
await client.add({
content: "Your content here",
containerTag: "project"
});
```
```python Python
client.add(
content="Your content here",
container_tags=["project"]
)
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Your content here", "containerTag": "project"}'
```
</CodeGroup>
### Upload File
`POST /v3/documents/file`
Upload files directly for processing.
<CodeGroup>
```typescript TypeScript
await client.documents.uploadFile({
file: fileStream,
containerTag: "project"
});
```
```python Python
client.documents.upload_file(
file=open('file.pdf', 'rb'),
container_tags='project'
)
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents/file" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-F "file=@document.pdf" \
-F "containerTags=project"
```
</CodeGroup>
### Update Memory
`PATCH /v3/documents/{id}`
Update existing document content or metadata. Content changes trigger reindexing; metadata-only updates do not.
<CodeGroup>
```typescript TypeScript
await client.documents.update("doc_id", {
content: "Updated content"
});
```
```python Python
client.documents.update("doc_id", {
"content": "Updated content"
})
```
```bash cURL
curl -X PATCH "https://api.supermemory.ai/v3/documents/doc_id" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Updated content"}'
```
</CodeGroup>
## Supported Content Types
### Documents
- PDF with OCR support
- Google Docs, Sheets, Slides
- Notion pages
- Microsoft Office files
### Media
- Images (JPG, PNG, GIF, WebP) with OCR
### Web Content
- Twitter/X posts
- YouTube videos with captions
### Text Formats
- Plain text
- Markdown
- CSV files
<Note> Refer to the [connectors guide](/connectors/overview) to learn how you can connect Google Drive, Notion, and OneDrive and sync files in real-time. </Note>
## Response Format
```json
{
"id": "D2Ar7Vo7ub83w3PRPZcaP1",
"status": "queued"
}
```
- **`id`**: Unique document identifier
- **`status`**: Processing state (`queued`, `processing`, `done`)
## Next Steps
- [Memory Operations](/memory-operations) - Track status, list, update, and delete memories
- [Search Memories](/search) - Search your content

View file

@ -0,0 +1,156 @@
---
title: "Parameters"
description: "Complete reference for add memory parameters"
---
Detailed parameter documentation for adding memories to Supermemory.
## Request Parameters
### Required Parameters
<ParamField body="content" type="string" required>
The content to process into memories. Can be:
- Plain text content
- URL to process
- HTML content
- Markdown text
```json
{
"content": "Machine learning is a subset of AI..."
}
```
**URL Examples:**
```json
{
"content": "https://youtube.com/watch?v=dQw4w9WgXcQ"
}
```
</ParamField>
### Optional Parameters
<ParamField body="containerTag" type="string">
**Recommended.** Single tag to group related memories. Improves search performance.
Default: `"sm_project_default"`
```json
{
"containerTag": "project_alpha"
}
```
<Note>
Use `containerTag` (singular) for better performance than `containerTags` (array).
</Note>
</ParamField>
<ParamField body="metadata" type="object">
Additional metadata as key-value pairs. Values must be strings, numbers, or booleans.
```json
{
"metadata": {
"source": "research-paper",
"author": "John Doe",
"priority": 1,
"reviewed": true
}
}
```
**Restrictions:**
- No nested objects
- No arrays as values
- Keys must be strings
- Values: string, number, or boolean only
</ParamField>
<ParamField body="customId" type="string">
Your own identifier for the document. Enables deduplication and updates.
**Maximum length:** 255 characters
```json
{
"customId": "doc_2024_01_research_ml"
}
```
**Use cases:**
- Prevent duplicate uploads
- Update existing documents
- Sync with external systems
</ParamField>
<ParamField body="raw" type="string">
Raw content to store alongside processed content. Useful for preserving original formatting.
```json
{
"content": "# Machine Learning\n\nML is a subset of AI...",
"raw": "# Machine Learning\n\nML is a subset of AI..."
}
```
</ParamField>
## File Upload Parameters
For `POST /v3/documents/file` endpoint:
<ParamField body="file" type="file" required>
The file to upload. Supported formats:
- **Documents:** PDF, DOC, DOCX, TXT, MD
- **Images:** JPG, PNG, GIF, WebP
- **Videos:** MP4, WebM, AVI
**Maximum size:** 50MB
</ParamField>
<ParamField body="containerTags" type="string">
Container tag for the uploaded file (sent as form field).
```bash
curl -X POST "https://api.supermemory.ai/v3/documents/file" \
-F "file=@document.pdf" \
-F "containerTags=research"
```
</ParamField>
## Container Tag Patterns
### Recommended Patterns
```typescript
// By user
"user_123"
// By project
"project_alpha"
// By organization and type
"org_456_research"
// By time period
"2024_q1_reports"
// By data source
"slack_channel_general"
```
### Performance Considerations
```typescript
// ✅ FAST: Single tag
{ "containerTag": "project_alpha" }
// ⚠️ SLOWER: Multiple tags
{ "containerTags": ["project_alpha", "backend", "auth"] }
// ❌ AVOID: Too many tags
{ "containerTags": ["tag1", "tag2", "tag3", "tag4", "tag5"] }
```

View file

@ -1,248 +0,0 @@
---
title: "Agents, skills and MCP"
description: "Set up coding agents to integrate Supermemory — CLI, skill, and docs MCP."
sidebarTitle: "Agents, skills and MCP"
icon: "bot"
---
This page is for **building with Supermemory** using coding agents: scaffolding a project, following the real API, and searching product docs.
It is **not** the consumer Memory MCP (give Claude/Cursor long-term memory about *you*). That is a separate product surface — see [Supermemory MCP](/supermemory-mcp/mcp).
| Path | How | For |
|---|---|---|
| **CLI** | `npx supermemory` | Setup, smoke tests, agent-driven integration |
| **Skill** | `npx skills add … --skill supermemory` | Teach the agent the real API surface |
| **Docs MCP** | `https://supermemory.ai/docs/mcp` | Search these docs while the agent codes |
## CLI
Agents (and humans) can set things up from the terminal easily using our CLI
```bash
npx supermemory
```
Useful for coding agents:
```bash
npx supermemory setup # detect project, launch/print integration flow
npx supermemory setup --prompt # print integration prompt only
npx supermemory setup --json # machine-readable output
npx supermemory help --json # agent-readable command catalog
npx supermemory help --all
```
Also available for smoke tests against your key: `add`, `search`, `profile`, `docs`, `tags`, `config`, `whoami`. Auth via first-run credentials or `SUPERMEMORY_API_KEY`.
```bash
npx supermemory add "User prefers TypeScript" --tag user_123
npx supermemory search "language preference" --tag user_123
npx supermemory profile --tag user_123
```
## Skill
Install the official skill so the agent uses the real endpoints, auth, and `containerTag` rules instead of hallucinating APIs:
```bash
npx skills add https://github.com/supermemoryai/skills --skill supermemory
```
Source: [github.com/supermemoryai/skills](https://github.com/supermemoryai/skills).
<Tip>
Best combo for coding agents: **skill** + **docs MCP** + **`npx supermemory setup`**.
</Tip>
## Docs MCP
Remote MCP that lets the agent **search Supermemory documentation** while it implements an integration.
Server URL:
```text
https://supermemory.ai/docs/mcp
```
### Setup by client
<Tabs>
<Tab title="Cursor">
Add to `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"supermemory-docs": {
"url": "https://supermemory.ai/docs/mcp"
}
}
}
```
</Tab>
<Tab title="Claude Code">
```bash
claude mcp add --transport http supermemory-docs https://supermemory.ai/docs/mcp
```
Or project `.mcp.json`:
```json
{
"mcpServers": {
"supermemory-docs": {
"type": "http",
"url": "https://supermemory.ai/docs/mcp"
}
}
}
```
</Tab>
<Tab title="Codex">
```bash
codex mcp add supermemory-docs --url https://supermemory.ai/docs/mcp
```
Or `~/.codex/config.toml`:
```toml
[mcp_servers.supermemory-docs]
url = "https://supermemory.ai/docs/mcp"
```
</Tab>
<Tab title="OpenCode">
```json
{
"mcp": {
"supermemory-docs": {
"type": "remote",
"url": "https://supermemory.ai/docs/mcp",
"enabled": true
}
}
}
```
</Tab>
<Tab title="VS Code">
Add to `.vscode/mcp.json`:
```json
{
"servers": {
"supermemory-docs": {
"type": "http",
"url": "https://supermemory.ai/docs/mcp"
}
}
}
```
</Tab>
<Tab title="Other">
```json
{
"mcpServers": {
"supermemory-docs": {
"url": "https://supermemory.ai/docs/mcp"
}
}
}
```
Stdio-only clients can proxy:
```json
{
"mcpServers": {
"supermemory-docs": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://supermemory.ai/docs/mcp"]
}
}
}
```
</Tab>
</Tabs>
### Starter prompt (docs + setup)
```text
You are integrating Supermemory into my app.
- Use the supermemory-docs MCP (or https://supermemory.ai/docs/llms.txt) before inventing endpoints.
- Prefer `npx supermemory setup` / the supermemory skill for correct auth, containerTag, and SDK usage.
- Canonical writes: POST /v3/documents · search: POST /v4/search · profile: POST /v4/profile
- Auth: Authorization: Bearer $SUPERMEMORY_API_KEY only
- Always scope with containerTag (singular) on write and search
- For demos use dreaming: "instant" when memories must be ready right after status done
```
### Integrate prompt (optional)
If the skill is not installed, paste a fuller prompt so the agent asks the right product questions:
<Accordion title="Copy full integration prompt" icon="copy">
````
You are integrating Supermemory into my application. Supermemory provides user memory, semantic search, and automatic knowledge extraction for AI applications.
Note: You can always reference the documentation by using the **supermemory-docs MCP** or content on **supermemory.ai/docs**. Prefer `npx supermemory setup` / `npx supermemory help --json` when scaffolding.
CANONICAL API SURFACE (use these, nothing else):
- Auth header: `Authorization: Bearer $SUPERMEMORY_API_KEY` — the only supported auth header
- Write content: POST https://api.supermemory.ai/v3/documents
- Search: POST https://api.supermemory.ai/v4/search
- Profile + search: POST https://api.supermemory.ai/v4/profile
- Settings: PATCH https://api.supermemory.ai/v3/settings
- Scoping: `containerTag` (singular string) in the JSON body — never in a header
- SDK: `client.add()`, `client.search()`, `client.profile()`
DO NOT USE — deprecated, undocumented, or fabricated:
- Endpoints: /v1/anything, /v3/memories, /v3/search (use /v3/documents and /v4/search)
- Headers: x-supermemory-api-key, x-api-key, x-sm-user-id (for API auth)
- Body keys: containerTags (plural) on writes as the only scope, userId, spaces
- Mixing: `rerank` and `rewriteQuery` on /v4/search only — never on /v3/search
SCOPING IS LOAD-BEARING. Every write and every search MUST include `containerTag`.
Prefer for tutorials:
- Ingest conversations with customId + dreaming: "instant" when you need memories immediately
- Wait until document status is done before search
- search with searchMode: "documents" for RAG, search (+ relatedMemories) for the graph, profile for always-on context
STEP 1: Ask what I'm building, integration style (AI SDK / OpenAI / Direct SDK / API), data model (user/org/both), profiles yes/no.
STEP 2: Install supermemory (npm/pip), set SUPERMEMORY_API_KEY from https://console.supermemory.ai
STEP 3: Generate complete working code.
DOCS: https://supermemory.ai/docs
````
</Accordion>
## Memory MCP (different product)
Want your **assistant** to remember you across chats (save/recall/profile in Claude, Cursor, etc.)? That is the **Memory MCP**, not the docs MCP:
→ [Supermemory MCP](/supermemory-mcp/mcp)
## Next steps
<CardGroup cols={2}>
<Card title="Quickstart" icon="play" href="/quickstart">
Conversation + document ingest, RAG, graph, profile, harness.
</Card>
<Card title="Memory MCP" icon="brain-circuit" href="/supermemory-mcp/mcp">
Persistent memory for assistants — separate from docs setup.
</Card>
<Card title="Plugins" icon="puzzle" href="/integrations/openclaw">
Claude Code, OpenClaw, Codex, Hermes, and more.
</Card>
<Card title="AI SDK" icon="triangle" href="/integrations/ai-sdk">
withSupermemory and memory tools in app code.
</Card>
</CardGroup>

View file

@ -0,0 +1,357 @@
---
title: "AI SDK Examples"
description: "Complete examples showing how to use Supermemory with Vercel AI SDK"
sidebarTitle: "Examples"
---
This page provides comprehensive examples of using Supermemory with the Vercel AI SDK, covering Memory Tools and User Profiles approaches.
## Personal Assistant with Memory Tools
Build an AI assistant that remembers user preferences and past interactions:
<CodeGroup>
```typescript Next.js API Route
import { streamText } from 'ai'
import { createAnthropic } from '@ai-sdk/anthropic'
import { supermemoryTools } from '@supermemory/tools/ai-sdk'
const anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY!
})
export async function POST(request: Request) {
const { messages } = await request.json()
const result = await streamText({
model: anthropic('claude-3-sonnet-20240229'),
messages,
tools: supermemoryTools(process.env.SUPERMEMORY_API_KEY!),
system: `You are a helpful personal assistant. When users share information about themselves,
remember it using the addMemory tool. When they ask questions, search your memories to provide
personalized responses. Always be proactive about remembering important details.`
})
return result.toAIStreamResponse()
}
```
```typescript Client Component
'use client'
import { useChat } from 'ai/react'
export default function PersonalAssistant() {
const { messages, input, handleInputChange, handleSubmit } = useChat()
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
<div className="flex-1 overflow-y-auto space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`p-4 rounded-lg ${
message.role === 'user' ? 'bg-blue-100 ml-auto' : 'bg-gray-100'
}`}
>
<p>{message.content}</p>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="mt-4">
<input
value={input}
onChange={handleInputChange}
placeholder="Tell me about yourself or ask me anything..."
className="w-full p-2 border rounded"
/>
</form>
</div>
)
}
```
</CodeGroup>
**Example conversation:**
- User: "I'm allergic to peanuts and I love Italian food"
- AI: *Uses addMemory tool* "I've remembered that you're allergic to peanuts and love Italian food!"
- User: "Suggest a restaurant for dinner"
- AI: *Uses searchMemories tool* "Based on what I know about you, I'd recommend an Italian restaurant that's peanut-free..."
## Customer Support with Context
Build a customer support system that remembers customer history:
```typescript
import { streamText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import { supermemoryTools } from '@supermemory/tools/ai-sdk'
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY!
})
export async function POST(request: Request) {
const { messages, customerId } = await request.json()
const result = await streamText({
model: openai('gpt-5'),
messages,
tools: supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
containerTags: [customerId]
}),
system: `You are a customer support agent. Before responding to any query:
1. Search for the customer's previous interactions and issues
2. Remember any new information shared in this conversation
3. Provide personalized help based on their history
4. Always be empathetic and solution-focused`
})
return result.toAIStreamResponse()
}
```
## Multi-User Learning Assistant
Build an assistant that learns from multiple users but keeps data separate:
```typescript
import { streamText } from 'ai'
import { createAnthropic } from '@ai-sdk/anthropic'
import { supermemoryTools } from '@supermemory/tools/ai-sdk'
const anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY!
})
export async function POST(request: Request) {
const { messages, userId, courseId } = await request.json()
const result = await streamText({
model: anthropic('claude-3-haiku-20240307'),
messages,
tools: supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
containerTags: [userId]
}),
system: `You are a learning assistant. Help students with their coursework by:
1. Remembering their learning progress and struggles
2. Searching for relevant information from their past sessions
3. Providing personalized explanations based on their learning style
4. Tracking topics they've mastered vs topics they need more help with`
})
return result.toAIStreamResponse()
}
```
## Research Assistant with File Processing
Combine file upload with memory tools for research assistance:
<CodeGroup>
```typescript API Route
import { streamText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import { supermemoryTools } from '@supermemory/tools/ai-sdk'
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY!
})
export async function POST(request: Request) {
const { messages, projectId } = await request.json()
const result = await streamText({
model: openai('gpt-5'),
messages,
tools: supermemoryTools(process.env.SUPERMEMORY_API_KEY!, {
containerTags: [projectId]
}),
system: `You are a research assistant. You can:
1. Search through uploaded research papers and documents
2. Remember key findings and insights from conversations
3. Help synthesize information across multiple sources
4. Track research progress and important discoveries`
})
return result.toAIStreamResponse()
}
```
```typescript File Upload Handler
import { addMemory } from '@supermemory/tools'
export async function POST(request: Request) {
const formData = await request.formData()
const file = formData.get('file') as File
const projectId = formData.get('projectId') as string
// Upload file and add to memory
const memory = await addMemory({
apiKey: process.env.SUPERMEMORY_API_KEY!,
content: file, // Supermemory handles file processing
title: file.name,
headers: {
'x-sm-conversation-id': projectId
}
})
return Response.json({
success: true,
message: "Document uploaded and processed for research",
memoryId: memory.id
})
}
```
</CodeGroup>
## Code Assistant with Project Memory
Create a coding assistant that remembers your codebase and preferences:
```typescript
import { streamText } from 'ai'
import { createAnthropic } from '@ai-sdk/anthropic'
import {
supermemoryTools,
searchMemoriesTool,
addMemoryTool
} from '@supermemory/tools/ai-sdk'
const anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY!
})
export async function POST(request: Request) {
const { messages, repositoryId } = await request.json()
const result = await streamText({
model: anthropic('claude-3-sonnet-20240229'),
messages,
tools: {
// Use individual tools for more control
searchMemories: searchMemoriesTool(process.env.SUPERMEMORY_API_KEY!, {
headers: {
'x-sm-conversation-id': `repo-${repositoryId}`
}
}),
addMemory: addMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
headers: {
'x-sm-conversation-id': `repo-${repositoryId}`
}
}),
// Add custom tools
executeCode: {
description: 'Execute code in a sandbox environment',
parameters: z.object({
code: z.string(),
language: z.string()
}),
execute: async ({ code, language }) => {
// Your code execution logic
return { result: "Code executed successfully" }
}
}
},
system: `You are a coding assistant with memory. You can:
1. Remember coding patterns and preferences from past conversations
2. Search through previous code examples and solutions
3. Track project architecture and design decisions
4. Learn from debugging sessions and common issues`
})
return result.toAIStreamResponse()
}
```
## Advanced: Custom Tool Integration
Combine Supermemory tools with your own custom tools:
```typescript
import { streamText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import { supermemoryTools } from '@supermemory/tools/ai-sdk'
import { z } from 'zod'
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY!
})
// Custom tool for calendar integration
const calendarTool = {
description: 'Create calendar events',
parameters: z.object({
title: z.string(),
date: z.string(),
duration: z.number()
}),
execute: async ({ title, date, duration }) => {
// Your calendar API integration
return { eventId: "cal_123", message: "Event created" }
}
}
export async function POST(request: Request) {
const { messages } = await request.json()
const result = await streamText({
model: openai('gpt-5'),
messages,
tools: {
// Spread Supermemory tools
...supermemoryTools(process.env.SUPERMEMORY_API_KEY!),
// Add custom tools
createEvent: calendarTool,
},
system: `You are a personal assistant that can remember information and
manage calendars. When users mention events or appointments:
1. Remember the details using addMemory
2. Create calendar events using createEvent
3. Search for conflicts using searchMemories`
})
return result.toAIStreamResponse()
}
```
## Environment Setup
For all examples, ensure you have these environment variables:
```bash .env.local
SUPERMEMORY_API_KEY=your_supermemory_key
OPENAI_API_KEY=your_openai_key
ANTHROPIC_API_KEY=your_anthropic_key
```
## Best Practices
### Memory Tools
- Use descriptive memory content for better search results
- Include context in your system prompts about when to use each tool
- Use project headers to separate different use cases
- Implement error handling for tool failures
### General Tips
- Start with simple examples and gradually add complexity
- Use the search functionality to avoid duplicate memories
- Implement proper authentication for production use
- Consider rate limiting for high-volume applications
## Next Steps
<CardGroup cols={2}>
<Card title="Memory API" icon="database" href="/memory-api/overview">
Advanced memory management with full API control
</Card>
<Card title="User Profiles" icon="user" href="/user-profiles">
Automatic personalization with user profiles
</Card>
</CardGroup>

View file

@ -0,0 +1,216 @@
---
title: "Infinite Chat"
description: "Unlimited context for chat applications with automatic memory management"
sidebarTitle: "Infinite Chat"
---
Infinite Chat provides unlimited context for chat applications with automatic memory management.
## Setup
```typescript
import { streamText } from "ai"
const infiniteChat = createAnthropic({
baseUrl: 'https://api.supermemory.ai/v3/https://api.anthropic.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
const result = await streamText({
model: infiniteChat("claude-3-sonnet"),
messages: [
{ role: "user", content: "Hello! Remember that I love TypeScript." }
]
})
```
## Provider Configuration
### Named Providers
<CodeGroup>
```typescript OpenAI
const infiniteChat = createOpenAI({
baseUrl: 'https://api.supermemory.ai/v3/https://api.openai.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
const result = await streamText({
model: infiniteChat("gpt-5"),
messages: [...]
})
```
```typescript Anthropic
const infiniteChat = createAnthropic({
baseUrl: 'https://api.supermemory.ai/v3/https://api.anthropic.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
const result = await streamText({
model: infiniteChat("claude-3-sonnet"),
messages: [...]
})
```
```typescript Google
const infiniteChat = createGoogleGenerativeAI({
baseUrl: 'https://api.supermemory.ai/v3/https://generativelanguage.googleapis.com/v1beta',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
const result = await streamText({
model: infiniteChat("gemini-pro"),
messages: [...]
})
```
```typescript Groq
const infiniteChat = createGroq({
baseUrl: 'https://api.supermemory.ai/v3/https://api.groq.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
const result = await streamText({
model: infiniteChat("mixtral-8x7b"),
messages: [...]
})
```
</CodeGroup>
### Custom Provider URL
```typescript
const infiniteChat = createOpenAI({
baseUrl: 'https://api.supermemory.ai/v3/https://api.openai.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
```
## Example Usage
```typescript
import { streamText } from "ai"
const infiniteChat = createOpenAI({
baseUrl: 'https://api.supermemory.ai/v3/https://api.openai.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
const result = await streamText({
model: infiniteChat("gpt-5"),
messages: [
{ role: "user", content: "What did we discuss yesterday?" }
]
})
return result.toAIStreamResponse()
```
## Configuration Options
```typescript
interface ConfigWithProviderName {
providerName: 'openai' | 'anthropic' | 'openrouter' |
'deepinfra' | 'groq' | 'google' | 'cloudflare'
providerApiKey: string
headers?: Record<string, string>
}
interface ConfigWithProviderUrl {
providerUrl: string
providerApiKey: string
headers?: Record<string, string>
}
```
### Custom Headers
Add user IDs, conversation IDs, or other metadata:
```typescript
const infiniteChat = createOpenAI({
baseUrl: 'https://api.supermemory.ai/v3/https://api.openai.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
```
## Comparison with Memory Tools
| Feature | Infinite Chat | Memory Tools |
|---------|--------------|--------------|
| Memory Management | Automatic | Manual |
| Context Handling | Automatic | Manual |
| Tool Calls | None | searchMemories, addMemory, fetchMemory |
| Best For | Chat apps | AI agents |
| Setup Complexity | Simple | Moderate |
## Headers
Add user and conversation context:
```typescript
const infiniteChat = createOpenAI({
baseUrl: 'https://api.supermemory.ai/v3/https://api.openai.com/v1',
apiKey: 'your-provider-api-key',
headers: {
'x-supermemory-api-key': 'supermemory-api-key',
'x-sm-conversation-id': 'conversation-id'
}
})
```
## Comparison
| Feature | Infinite Chat | Memory Tools |
|---------|--------------|-------------|
| Memory Management | Automatic | Manual |
| Context Handling | Automatic | Manual |
| Tool Calls | None | searchMemories, addMemory, fetchMemory |
| Best For | Chat apps | AI agents |
## Next Steps
<CardGroup cols={2}>
<Card title="Memory Tools" icon="wrench" href="/integrations/ai-sdk">
Explore explicit memory control
</Card>
<Card title="Examples" icon="code" href="/cookbook/ai-sdk-integration">
See complete implementations
</Card>
</CardGroup>

View file

@ -0,0 +1,147 @@
---
title: "Memory Tools"
description: "Add memory capabilities to your AI agents with Vercel AI SDK tools"
sidebarTitle: "Memory Tools"
---
Memory tools allow AI agents to search, add, and fetch memories.
## Setup
```typescript
import { streamText } from "ai"
import { createOpenAI } from "@ai-sdk/openai"
import { supermemoryTools } from "@supermemory/tools/ai-sdk"
const openai = createOpenAI({
apiKey: "YOUR_OPENAI_KEY"
})
const result = await streamText({
model: openai("gpt-5"),
prompt: "Remember that my name is Alice",
tools: supermemoryTools("YOUR_SUPERMEMORY_KEY")
})
```
## Available Tools
### Search Memories
Semantic search through user memories:
```typescript
const result = await streamText({
model: openai("gpt-5"),
prompt: "What are my dietary preferences?",
tools: supermemoryTools("API_KEY")
})
// The AI will automatically call searchMemories tool
// Example tool call:
// searchMemories({ informationToGet: "dietary preferences and restrictions" })
```
### Add Memory
Store new information:
```typescript
const result = await streamText({
model: anthropic("claude-3-sonnet"),
prompt: "Remember that I'm allergic to peanuts",
tools: supermemoryTools("API_KEY")
})
// The AI will automatically call addMemory tool
// Example tool call:
// addMemory({ memory: "User is allergic to peanuts" })
```
### Fetch Memory
Retrieve specific memory by ID:
```typescript
const result = await streamText({
model: openai("gpt-5"),
prompt: "Get the details of memory abc123",
tools: supermemoryTools("API_KEY")
})
// The AI will automatically call fetchMemory tool
// Example tool call:
// fetchMemory({ memoryId: "abc123" })
```
## Using Individual Tools
For more control, import tools separately:
```typescript
import {
searchMemoriesTool,
addMemoryTool,
fetchMemoryTool
} from "@supermemory/tools/ai-sdk"
// Use only search tool
const result = await streamText({
model: openai("gpt-5"),
prompt: "What do you know about me?",
tools: {
searchMemories: searchMemoriesTool("API_KEY", {
projectId: "personal"
})
}
})
// Combine with custom tools
const result = await streamText({
model: anthropic("claude-3"),
prompt: "Help me with my calendar",
tools: {
searchMemories: searchMemoriesTool("API_KEY"),
// Your custom tools
createEvent: yourCustomTool,
sendEmail: anotherCustomTool
}
})
```
## Tool Results
Each tool returns a result object:
```typescript
// searchMemories result
{
success: true,
results: [...], // Array of memories
count: 5
}
// addMemory result
{
success: true,
memory: { id: "mem_123", ... }
}
// fetchMemory result
{
success: true,
memory: { id: "mem_123", content: "...", ... }
}
```
## Next Steps
<CardGroup cols={2}>
<Card title="User Profiles" icon="user" href="/integrations/ai-sdk">
Automatic personalization with profiles
</Card>
<Card title="Examples" icon="code" href="/cookbook/ai-sdk-integration">
See more complete examples
</Card>
</CardGroup>

5
apps/docs/ai-sdk/npm.mdx Normal file
View file

@ -0,0 +1,5 @@
---
title: "NPM link"
url: "https://www.npmjs.com/package/@supermemory/tools"
icon: npm
---

View file

@ -0,0 +1,93 @@
---
title: "AI SDK Integration"
description: "Use Supermemory with Vercel AI SDK for seamless memory management"
sidebarTitle: "Overview"
---
The Supermemory AI SDK provides native integration with Vercel's AI SDK through two approaches: **User Profiles** for automatic personalization and **Memory Tools** for agent-based interactions.
<Card title="Supermemory tools on npm" icon="npm" href="https://www.npmjs.com/package/@supermemory/tools">
Check out the NPM page for more details
</Card>
## Installation
```bash
npm install @supermemory/tools
```
## User Profiles with Middleware
Automatically inject user profiles into every LLM call for instant personalization. Customize how memories are formatted with the `promptTemplate` option for XML-based prompting, custom branding, or model-specific formatting.
```typescript
import { generateText } from "ai"
import { withSupermemory } from "@supermemory/tools/ai-sdk"
import { openai } from "@ai-sdk/openai"
// Wrap your model with Supermemory - profiles are automatically injected
const modelWithMemory = withSupermemory(openai("gpt-5"), {
containerTag: "user-123",
customId: "conversation-456",
})
const result = await generateText({
model: modelWithMemory,
messages: [{ role: "user", content: "What do you know about me?" }]
})
// The model automatically has the user's profile context!
```
<Note>
**Memory saving is enabled by default** (`addMemory: "always"`). New conversations are persisted automatically. To opt out, set `addMemory: "never"`:
```typescript
const modelWithMemory = withSupermemory(openai("gpt-5"), {
containerTag: "user-123",
customId: "conversation-456",
addMemory: "never",
})
```
</Note>
```typescript
```
## Memory Tools
Add memory capabilities to AI agents with search, add, and fetch operations.
```typescript
import { streamText } from "ai"
import { createAnthropic } from "@ai-sdk/anthropic"
import { supermemoryTools } from "@supermemory/tools/ai-sdk"
const anthropic = createAnthropic({
apiKey: "YOUR_ANTHROPIC_KEY"
})
const result = await streamText({
model: anthropic("claude-3-sonnet"),
prompt: "Remember that my name is Alice",
tools: supermemoryTools("YOUR_SUPERMEMORY_KEY")
})
```
## When to Use
| Approach | Use Case |
|----------|----------|
| User Profiles | Personalized LLM responses with automatic user context |
| Memory Tools | AI agents that need explicit memory control |
## Next Steps
<CardGroup cols={2}>
<Card title="User Profiles" icon="user" href="/integrations/ai-sdk">
Automatic personalization with profiles
</Card>
<Card title="Memory Tools" icon="wrench" href="/integrations/ai-sdk">
Agent-based memory management
</Card>
</CardGroup>

View file

@ -0,0 +1,357 @@
---
title: "User Profiles with AI SDK"
description: "Automatically inject user profiles into LLM calls for instant personalization"
sidebarTitle: "User Profiles"
---
## Overview
The `withSupermemory` middleware automatically injects user profiles into your LLM calls, providing instant personalization without manual prompt engineering or API calls.
<Note>
**New to User Profiles?** Read the [conceptual overview](/user-profiles) to understand what profiles are and why they're powerful for LLM personalization.
</Note>
## Quick Start
```typescript
import { generateText } from "ai"
import { withSupermemory } from "@supermemory/tools/ai-sdk"
import { openai } from "@ai-sdk/openai"
// Wrap any model with Supermemory middleware
const modelWithMemory = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conversation-456",
})
// Use normally - profiles are automatically injected!
const result = await generateText({
model: modelWithMemory,
messages: [{ role: "user", content: "Help me with my current project" }]
})
// The model knows about the user's background, skills, and current work!
```
## How It Works
The `withSupermemory` middleware:
1. **Intercepts** your LLM calls before they reach the model
2. **Fetches** the user's profile based on the container tag
3. **Injects** profile data into the system prompt automatically
4. **Forwards** the enhanced prompt to your LLM
All of this happens transparently - you write code as if using a normal model, but get personalized responses.
<Note>
**Memory saving is enabled by default** (`addMemory: "always"`). New conversations are persisted automatically. To opt out, set `addMemory: "never"`:
```typescript
const model = withSupermemory(openai("gpt-5"), {
containerTag: "user-123",
customId: "conversation-456",
addMemory: "never",
})
```
</Note>
## Memory Search Modes
Configure how the middleware retrieves and uses memory:
### Profile Mode (Default)
Retrieves the user's complete profile without query-specific search. Best for general personalization.
```typescript
// Default behavior - profile mode
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
})
// Or explicitly specify
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
mode: "profile",
})
const result = await generateText({
model,
messages: [{ role: "user", content: "What do you know about me?" }]
})
// Response uses full user profile for context
```
### Query Mode
Searches memories based on the user's specific message. Best for finding relevant information.
```typescript
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
mode: "query",
})
const result = await generateText({
model,
messages: [{
role: "user",
content: "What was that Python script I wrote last week?"
}]
})
// Searches for memories about Python scripts from last week
```
### Full Mode
Combines profile AND query-based search for comprehensive context. Best for complex interactions.
```typescript
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
mode: "full",
})
const result = await generateText({
model,
messages: [{
role: "user",
content: "Help me debug this similar to what we did before"
}]
})
// Uses both profile (user's expertise) AND search (previous debugging sessions)
```
## Custom Prompt Templates
Customize how memories are formatted and injected into the system prompt using the `promptTemplate` option. This is useful for:
- Using XML-based prompting (e.g., for Claude models)
- Custom branding (removing "supermemories" references)
- Controlling how your agent describes where information comes from
```typescript
import { generateText } from "ai"
import { withSupermemory, type MemoryPromptData } from "@supermemory/tools/ai-sdk"
import { openai } from "@ai-sdk/openai"
const customPrompt = (data: MemoryPromptData) => `
<user_memories>
Here is some information about your past conversations with the user:
${data.userMemories}
${data.generalSearchMemories}
</user_memories>
`.trim()
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
mode: "full",
promptTemplate: customPrompt,
})
const result = await generateText({
model,
messages: [{ role: "user", content: "What do you know about me?" }]
})
```
### MemoryPromptData Interface
The `MemoryPromptData` object passed to your template function provides:
- `userMemories`: Pre-formatted markdown combining static profile facts (name, preferences, goals) and dynamic context (current projects, recent interests)
- `generalSearchMemories`: Pre-formatted search results based on semantic similarity to the current query (empty string if mode is "profile")
- `searchResults`: Raw search results array (`Array<{ memory: string; metadata?: Record<string, unknown> }>`) for traversing, filtering, or selectively including results based on metadata
### XML-Based Prompting for Claude
Claude models perform better with XML-structured prompts:
```typescript
const claudePrompt = (data: MemoryPromptData) => `
<context>
<user_profile>
${data.userMemories}
</user_profile>
<relevant_memories>
${data.generalSearchMemories}
</relevant_memories>
</context>
Use the above context to provide personalized responses.
`.trim()
const model = withSupermemory(anthropic("claude-3-sonnet"), {
containerTag: "user-123",
customId: "conv-1",
mode: "full",
promptTemplate: claudePrompt,
})
```
### Filtering Search Results
Use `searchResults` to traverse the raw data and pick what's important:
```typescript
const selectivePrompt = (data: MemoryPromptData) => {
const relevant = data.searchResults.filter(
(r) => (r.metadata?.score as number) > 0.7
)
return `
<user_memories>
${data.userMemories}
</user_memories>
<relevant_context>
${relevant.map((r) => `- ${r.memory}`).join("\n")}
</relevant_context>
`.trim()
}
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
mode: "full",
promptTemplate: selectivePrompt,
})
```
### Custom Branding
Remove "supermemories" references and use your own branding:
```typescript
const brandedPrompt = (data: MemoryPromptData) => `
You are an AI assistant with access to the user's personal knowledge base.
User Profile:
${data.userMemories}
Relevant Context:
${data.generalSearchMemories}
Use this information to provide personalized and contextually relevant responses.
`.trim()
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
promptTemplate: brandedPrompt,
})
```
### Default Template
If no `promptTemplate` is provided, the default format is used:
```typescript
const defaultPrompt = (data: MemoryPromptData) =>
`User Supermemories: \n${data.userMemories}\n${data.generalSearchMemories}`.trim()
```
## Verbose Logging
Enable detailed logging to see exactly what's happening:
```typescript
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
verbose: true, // Enable detailed logging
})
const result = await generateText({
model,
messages: [{ role: "user", content: "Where do I live?" }]
})
// Console output:
// [supermemory] Searching memories for container: user-123
// [supermemory] User message: Where do I live?
// [supermemory] System prompt exists: false
// [supermemory] Found 3 memories
// [supermemory] Memory content: You live in San Francisco, California...
// [supermemory] Creating new system prompt with memories
```
## Comparison with Direct API
The AI SDK middleware abstracts away the complexity of manual profile management:
<Tabs>
<Tab title="With AI SDK (Simple)">
```typescript
// Simple setup
const model = withSupermemory(openai("gpt-4"), {
containerTag: "user-123",
customId: "conv-1",
})
// Use normally
const result = await generateText({
model,
messages: [{ role: "user", content: "Help me" }]
})
```
</Tab>
<Tab title="Without AI SDK (Complex)">
```typescript
// Manual profile fetching
const profileRes = await fetch('https://api.supermemory.ai/v4/profile', {
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({ containerTag: "user-123" })
})
const profile = await profileRes.json()
// Manual prompt construction
const systemPrompt = `User Profile:\n${profile.profile.static?.join('\n')}`
// Manual LLM call with profile
const result = await generateText({
model: openai("gpt-4"),
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: "Help me" }
]
})
```
</Tab>
</Tabs>
## Limitations
- **Beta Feature**: The `withSupermemory` middleware is currently in beta
- **Container Tag Required**: You must provide a valid container tag
- **API Key Required**: Ensure `SUPERMEMORY_API_KEY` is set in your environment
## Next Steps
<CardGroup cols={2}>
<Card title="User Profiles Concepts" icon="brain" href="/user-profiles">
Understand how profiles work conceptually
</Card>
<Card title="Memory Tools" icon="wrench" href="/integrations/ai-sdk">
Add explicit memory operations to your agents
</Card>
<Card title="API Reference" icon="code" href="https://api.supermemory.ai/v3/reference#tag/profile">
Explore the underlying profile API
</Card>
<Card title="NPM Package" icon="npm" href="https://www.npmjs.com/package/@supermemory/tools">
View the package on NPM
</Card>
</CardGroup>
<Info>
**Pro Tip**: Start with profile mode for general personalization, then experiment with query and full modes as you understand your use case better.
</Info>

View file

@ -1,17 +0,0 @@
---
title: "Connections"
sidebarTitle: "Overview"
description: "External connectors — create, configure, sync, and manage resources."
icon: "book-open"
---
Connections pull content from Notion, Google Drive, Gmail, OneDrive, S3, GitHub, and more.
| Area | Endpoints |
| --- | --- |
| Create / delete | `POST/DELETE /v3/connections/{provider}` |
| List / get | `POST /v3/connections/list`, `GET …/{connectionId}` |
| Configure / resources | `POST …/configure`, `GET …/resources` |
| Sync / documents | `POST …/import`, `POST …/documents` |
**Guides:** [Connectors overview](/connectors/overview) · provider pages under Connectors

View file

@ -1,18 +0,0 @@
---
title: "Container tags"
sidebarTitle: "Overview"
description: "Multi-tenant containers — settings, merge, and delete."
icon: "book-open"
---
`containerTag` is the primary multi-tenant key (user id, workspace id, etc.). These endpoints manage settings and lifecycle for a tag.
| Endpoint | Use when |
| --- | --- |
| `GET /v3/container-tags/{containerTag}` | Read tag settings |
| `PATCH /v3/container-tags/{containerTag}` | Update tag settings |
| `DELETE /v3/container-tags/{containerTag}` | Delete a container and its data |
| `POST /v3/container-tags/merge` | Merge one tag into another |
| `GET /v3/container-tags/merge/{mergeId}` | Poll merge status |
**Guide:** [Container tags](/concepts/container-tags) · [Filtering](/concepts/filtering)

View file

@ -1,21 +0,0 @@
---
title: "Documents"
sidebarTitle: "Overview"
description: "List, get status, update, delete, and inspect ingested documents."
icon: "book-open"
---
Documents are the unit of ingestion. Adds return immediately with `status: "queued"`; poll until `done` before relying on search or profiles.
| Endpoint | Use when |
| --- | --- |
| `GET /v3/documents/{id}` | Status + metadata for one document |
| `POST /v3/documents/list` | Filter and paginate documents |
| `GET /v3/documents/processing` | Currently processing items |
| `PATCH /v3/documents/{id}` | Update content or metadata |
| `DELETE /v3/documents/{id}` | Delete by id or customId |
| `DELETE /v3/documents/bulk` | Bulk delete |
| `GET /v3/documents/{id}/chunks` | Inspect RAG chunks |
| `GET /v3/documents/{id}/file-url` | Presigned URL for uploaded files |
**Guide:** [Document operations](/ingestion/document-operations)

View file

@ -1,21 +0,0 @@
---
title: "Ingest"
sidebarTitle: "Overview"
description: "Add documents, files, batches, and conversations to Supermemory."
icon: "book-open"
---
Send raw content into the processing pipeline. Supermemory extracts memories, chunks for RAG, and updates profiles asynchronously.
| Endpoint | Use when |
| --- | --- |
| `POST /v3/documents` | Text, URLs, or structured content |
| `POST /v3/documents/file` | Binary file upload |
| `POST /v3/documents/batch` | Many documents in one request |
| `POST /v4/conversations` | Chat sessions with turn-aware ingest |
**Guides:** [Add memories](/ingestion/add-memories) · [Quickstart](/quickstart)
<Tip>
Use a stable `customId` (conversation id, doc id) so re-sends upsert instead of duplicating. Pass `dreaming: "instant"` when the next step is memory search or profiles.
</Tip>

View file

@ -1,20 +0,0 @@
---
title: "Memories"
sidebarTitle: "Overview"
description: "Create, list, update, and forget extracted memory entries (v4)."
icon: "book-open"
---
These endpoints operate on **extracted memories**, not raw documents.
| Endpoint | Use when |
| --- | --- |
| `POST /v4/memories` | Write memories directly (skip document pipeline) |
| `POST /v4/memories/list` | List with history / versions |
| `PATCH /v4/memories` | Update (creates a new version) |
| `DELETE /v4/memories` | Forget a specific memory |
| `POST /v4/memories/forget-matching` | Forget by natural-language match |
For document-level CRUD, use [Documents](/api-reference/documents). For pipeline ingest, use [Ingest](/api-reference/ingest).
**Guide:** [Memory operations](/recall/memory-operations)

View file

@ -1,68 +0,0 @@
---
title: "API Reference"
description: "Interactive reference for the Supermemory HTTP API — ingest, search, profiles, memories, connectors, and settings."
icon: "unplug"
---
This is the **contract-level** reference for Supermemory: methods, paths, parameters, and the playground.
For narrative guides (when to use what, patterns, SDKs), start with the [Quickstart](/quickstart) and [Using supermemory](/ingestion/add-memories).
## Base URL
```
https://api.supermemory.ai
```
Self-hosted: use your instance URL (for example `http://localhost:6767`). See [Self-hosting](/self-hosting/overview).
## Authentication
All endpoints use a Bearer API key. Create one in the [developer console](https://console.supermemory.ai).
```bash
Authorization: Bearer sm_...
```
Details: [API keys & auth](/authentication).
## Mental model
| Group | What it does |
| --- | --- |
| **Ingest** | Add documents, files, batches, and conversations into the pipeline |
| **Documents** | Get status, list, update, delete, chunks, and file URLs |
| **Search** | Semantic recall — memories, documents, or hybrid |
| **Profiles** | Static + dynamic facts for a container (user / entity) |
| **Memories** | Create, list, update, and forget extracted memory entries |
| **Container tags** | Multi-tenant settings, merge, and delete for a container |
| **Connections** | OAuth connectors (Drive, Notion, Gmail, …) and sync |
| **Settings** | Org-level customization, buckets, and reset |
Same `containerTag` scopes ingest, search, and profiles — one engine, multiple ways out.
## Suggested order
1. **Ingest** — `POST /v3/documents` (SDK: `client.add`)
2. **Documents** — `GET /v3/documents/{id}` until `status: "done"`
3. **Search** — `POST /v4/search`
4. **Profiles** — `POST /v4/profile`
Full walkthrough with conversation + document examples: [Quickstart](/quickstart).
## SDKs
Official clients wrap this API:
- TypeScript: `npm install supermemory`
- Python: `pip install supermemory`
See [Supermemory SDK](/integrations/supermemory-sdk).
Playground snippets come from the OpenAPI spec: official **TypeScript / Python SDK** samples via `x-codeSamples`, plus cURL. (After API deploy — until then you may still see generic HTTP snippets.)
SDK generation is migrating off Stainless SaaS to **stlc** soon; documented OpenAPI samples will then be produced by the SDK build instead of a hand-maintained map.
## OpenAPI
Spec (live): [https://api.supermemory.ai/v3/openapi](https://api.supermemory.ai/v3/openapi)

View file

@ -1,15 +0,0 @@
---
title: "Profiles"
sidebarTitle: "Profiles overview"
description: "Entity profiles — static and dynamic facts for a container."
icon: "id-card"
---
Profiles summarize what Supermemory knows about a user or entity in a `containerTag`.
| Endpoint | Use when |
| --- | --- |
| `POST /v4/profile` | Fetch static + dynamic profile for a container |
| `POST /v4/profile/buckets` | Profile organized by custom buckets |
**Guides:** [User profiles API](/recall/user-profiles) · [Concepts](/concepts/user-profiles) · [Buckets](/user-profiles/buckets)

View file

@ -1,19 +0,0 @@
---
title: "Recall"
sidebarTitle: "Overview"
description: "Semantic search over memories, document chunks, or both — plus user profiles."
icon: "book-open"
---
Get context back out of Supermemory: search extracted memories / documents, or fetch a user profile.
| Endpoint | Role |
| --- | --- |
| `POST /v4/search` | Primary recall — `searchMode`: `memories`, `documents`, or `hybrid` |
| `POST /v3/search` | Document / SuperRAG-oriented search |
| `POST /v4/profile` | Static + dynamic profile for a container |
| `POST /v4/profile/buckets` | Profile organized by custom buckets |
Prefer **v4** with `searchMode: "hybrid"` unless you only need document chunks or only extracted memories.
**Guides:** [Search](/recall/search) · [User profiles](/recall/user-profiles) · [SuperRAG](/concepts/super-rag) · [Memory vs RAG](/concepts/memory-vs-rag)

View file

@ -1,17 +0,0 @@
---
title: "Settings"
sidebarTitle: "Overview"
description: "Organization settings, profile buckets, and data reset."
icon: "book-open"
---
Org-level configuration for extraction, customization, and profile buckets.
| Endpoint | Use when |
| --- | --- |
| `GET /v3/settings` | Read org settings |
| `PATCH /v3/settings` | Update org settings |
| `POST /v3/settings/suggest-buckets` | Suggest profile buckets |
| `POST /v3/settings/reset` | Reset organization data (destructive) |
**Guide:** [Customization](/concepts/customization)

View file

@ -1,7 +1,6 @@
--- ---
title: "API keys & auth" title: "Authentication"
description: "Org API keys, container-scoped keys, and connector branding." description: "API keys, scoped keys, and connector branding."
sidebarTitle: "API keys"
icon: "key" icon: "key"
--- ---
@ -56,20 +55,17 @@ This works for Google Drive, Notion, and OneDrive. See the full setup in [Custom
--- ---
## Scoped API keys ## Scoped API Keys
Scoped keys are restricted to one or more `containerTag`s. They can only access documents and search within those containers — use them to give a client, session, or tenant limited access without shipping your org master key. <Accordion title="Container-scoped keys" icon="lock">
Scoped keys are restricted to a single `containerTag`. They can only access documents and search within that container — useful for giving limited access to specific projects, users, or tenants without exposing your full API key.
Pairs with [container tags](/concepts/container-tags) for multi-tenant isolation. **Allowed endpoints:** `/v3/documents`, `/v3/memories`, `/v4/memories`, `/v3/search`, `/v4/search`, `/v4/profile`
**Allowed endpoints:** `/v3/documents`, `/v3/memories`, `/v4/memories`, `/v3/search`, `/v4/search`, `/v4/profile` ### Create a scoped key
Scoped keys **cannot** read billing, manage org settings, or mint further keys. ```bash
curl https://api.supermemory.ai/v3/auth/scoped-key \
### Create a scoped key
```bash
curl https://api.supermemory.ai/v3/auth/scoped-key \
--request POST \ --request POST \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Authorization: Bearer YOUR_API_KEY' \
@ -78,43 +74,46 @@ curl https://api.supermemory.ai/v3/auth/scoped-key \
"name": "my-key-name", "name": "my-key-name",
"expiresInDays": 30 "expiresInDays": 30
}' }'
``` ```
### Parameters ### Parameters
| Parameter | Required | Default | Description | | Parameter | Required | Default | Description |
| --- | --- | --- | --- | | --------------------- | -------- | ----------------------- | ------------------------------------------------ |
| `containerTag` | Yes | — | Alphanumeric, hyphens, underscores, colons, dots | | `containerTag` | Yes | — | Alphanumeric, hyphens, underscores, colons, dots |
| `name` | No | `scoped_{containerTag}` | Display name for the key | | `name` | No | `scoped_{containerTag}` | Display name for the key |
| `expiresInDays` | No | — | 1365 days | | `expiresInDays` | No | — | 1365 days |
| `rateLimitMax` | No | `500` | Max requests per window (110,000) | | `rateLimitMax` | No | `500` | Max requests per window (110,000) |
| `rateLimitTimeWindow` | No | `60000` | Window in milliseconds (13,600,000) | | `rateLimitTimeWindow` | No | `60000` | Window in milliseconds (13,600,000) |
### Response ### Response
```json ```json
{ {
"key": "sm_orgId_...", "key": "sm_orgId_...",
"id": "key-id", "id": "key-id",
"name": "scoped_my-project", "name": "scoped_my-project",
"containerTag": "my-project", "containerTag": "my-project",
"expiresAt": "2026-03-08T00:00:00.000Z", "expiresAt": "2026-03-08T00:00:00.000Z",
"allowedEndpoints": ["/v3/documents", "/v3/memories", "/v4/memories", "/v3/search", "/v4/search", "/v4/profile"] "allowedEndpoints": ["/v3/documents", "/v3/memories", "/v4/memories", "/v3/search", "/v4/search", "/v4/profile"]
} }
``` ```
Use the returned key like a normal API key — it just will not work outside its container scope. Use the returned key exactly like a normal API key — it just won't work outside its container scope.
### Disable a scoped key ### Disable a scoped key
Revoke with the `id` from creation. Subsequent requests get `401`. Memories and container tags are **not** deleted. To revoke a scoped key, send a `DELETE` request with the `id` returned at creation time. This disables the key immediately — any subsequent requests using it will get a `401`. Memories and container tags are **not** affected.
```bash ```bash
curl https://api.supermemory.ai/v3/auth/scoped-key/KEY_ID \ curl https://api.supermemory.ai/v3/auth/scoped-key/KEY_ID \
--request DELETE \ --request DELETE \
--header 'Authorization: Bearer YOUR_API_KEY' --header 'Authorization: Bearer YOUR_API_KEY'
``` ```
```json **Response:**
{ "success": true }
``` ```json
{ "success": true }
```
</Accordion>

View file

@ -0,0 +1,212 @@
---
title: "Developer Platform"
description: "API updates, new endpoints, and SDK releases"
---
API updates, new endpoints, SDK releases, and developer-focused features.
## April 13, 2026
- **Google Drive scoped sync:** New connections default to a **hosted folder/file picker** after OAuth; only chosen items sync. Use `metadata.syncScope: "full"` to sync the whole Drive. Import jobs **skip** scoped connections until a selection exists.
## March 18, 2026
- **Supermemory CLI:** New command-line tool for managing memories, documents, profiles, tags, connectors, and API keys directly from the terminal.
- **PPTX Support:** PowerPoint files (`.pptx`) are now a supported content type for ingestion.
- **Multiple containerTags on Scoped API Keys:** Scoped API keys can now be assigned to multiple container tags, allowing a single key to access several spaces.
- **Documents Page in Console:** New dedicated documents browser in the console for viewing, filtering, and managing all ingested content.
- **`@supermemory/tools` v1.4.1:** Now exposes raw `searchResults` in `MemoryPromptData`, giving full control over how retrieved memories are formatted in prompts.
## March 12, 2026
- **Audio Extraction:** Ingest audio files with automatic transcription powered by Gemini 2.5 Flash. Audio content is transcribed, chunked, and indexed like any other document.
- **Delete Connection Without Documents:** Disconnect an external source (Google Drive, Notion, etc.) without deleting the documents it synced.
- **Org-Level Overage Toggle:** Control overage billing per-organization with a new toggle in the billing settings.
- **Retry Failed Documents:** Documents that previously failed ingestion can now be retried by re-submitting with the same `customId`.
- **Copyable Team Invite Link:** Team management page now includes a shareable invite link.
## March 9, 2026
- **Delete Scoped API Keys:** New `DELETE` endpoint to disable scoped API keys programmatically.
- **`supermemory-agent-framework` Python Package:** Official Python package for using Supermemory with Microsoft's Agent Framework — memory tools and middleware out of the box.
- **OpenAI SDK Backfill:** Improved compatibility across `supermemory-openai-sdk` (Python) and `@supermemory/tools` (TypeScript) OpenAI integrations.
- **Bulk Delete in Nova:** Bulk document deletion now available in the Nova app interface.
## March 5, 2026
- **`extends` Relation Type:** Memory graph now supports `extends` as a relation type, enabling richer knowledge graph connections between documents.
- **Interactive Memory Graph in MCP:** The MCP server now includes an interactive graph visualization app for exploring memory connections from any MCP-compatible client.
- **Plugin Auth Connect Page:** New OAuth-style connect page for plugin integrations (Claude Code, OpenCode, OpenClaw).
- **ViaSocket Integration:** New integration guide for connecting Supermemory with ViaSocket automation workflows.
## March 2, 2026
- **Configurable Vector Stores:** Bring your own vector store — Supermemory now supports pluggable vector backends beyond the default.
- **List Memories Endpoint:** New `GET /v3/documents` endpoint with pagination, filtering by container tag, status, and metadata.
## February 26, 2026
- **Self-Hostable Supermemory:** Run the full Supermemory stack on your own infrastructure with Docker.
- **Console v2:** Complete redesign of the developer console with new navigation, improved billing, and a unified project view.
- **No More 120 Memory Limit:** The previous cap of 120 memories per container tag has been removed. Store unlimited memories.
## February 22, 2026
- **Supermemory Skill for Claude Code:** Install with `npx skills add supermemoryai/skills` — teaches Claude to proactively recommend and implement Supermemory when building AI apps that need persistent memory, user profiles, or semantic search. Includes ready-to-use TypeScript and Python examples.
- **Metadata Filtering for Profiles:** User profile search now supports metadata-based filtering for more targeted profile queries.
- **List Documents with Multiple Container Tags:** New `operator` parameter to query documents spanning multiple container tags.
- **Deprecate `include: chunks`:** The `include: chunks` parameter in `/v4/search` is deprecated in favor of the `hybrid` search mode.
## February 9, 2026
- **Unified Organizations:** Consumer and developer organizations merged into a single org type. All orgs can now access both Nova and the developer API.
- **Credits-Based Usage Display:** Billing now shows token usage in a credits-based format.
- **Nova Spaces with Multi-Select:** Spaces in Nova now support multi-select, replacing "All Spaces" with scoped "Nova Spaces."
## February 6, 2026
- **Scoped API Keys for Container Tags:** Create API keys scoped to specific container tags for fine-grained access control per space.
- **DELETE Endpoint for Container Tags:** New endpoint to delete container tags and their associated document relationships.
- **Container Tag-Level Context Prompts:** Set custom context prompts per container tag to control how memories are extracted and summarized within each space.
## February 3, 2026
- **New Integration Docs:** Added guides for LangGraph, OpenAI Agents SDK, CrewAI, Agno, Mastra, and LangChain — covering all major AI agent frameworks.
- **Claude Code Integration:** Official integration page for using Supermemory as persistent memory in Claude Code.
- **Entity Context Documentation:** New docs on how entity extraction and context enrichment work in the memory pipeline.
- **Authentication Docs:** Comprehensive authentication page with code examples for API key auth, OAuth, and scoped keys.
## January 25, 2026
- **Plugin Authentication System:** New auth system for external tool integrations, enabling secure plugin-to-API connections.
- **Enterprise Plan Support:** Enterprise tier now available in the console with dedicated billing and support options.
- **Plugin Catalog:** Dedicated plugin page with auth flows for Claude Code, OpenCode, and OpenClaw integrations.
- **`@supermemory/tools` — Strict Mode:** Strict mode support for OpenAI function calling, ensuring schema-validated tool calls.
## January 14, 2026
- **Hybrid PDF Pipeline:** PDF extraction now uses Mistral OCR 3 with Gemini fallback for significantly improved accuracy on scanned documents and complex layouts.
- **Halfvec Embeddings:** Embedding storage optimized with half-precision vectors, reducing storage costs while maintaining search quality.
- **Spaces Creation with Emoji:** Create and customize spaces with emoji identifiers in Nova.
## January 8, 2026
- **Gmail Connector:** New connector to sync Gmail threads into Supermemory. Threads are stored in R2 for reliable processing of large mailboxes.
- **Container Tag Filters:** Filter documents by container tag in list and search endpoints.
- **Pagination Improvements:** Improved pagination and document view across the console.
- **`supermemory-pipecat` Python Package:** New SDK for integrating Supermemory with Pipecat voice AI pipelines, including Gemini Live speech-to-speech support.
- **`@supermemory/tools` — Prompt Templates:** Customize how memory context is formatted in AI SDK integrations with the new `promptTemplate` option.
## December 30, 2025
- **MCP 4.0:** Major MCP server update with session configuration, project-aware tools on every init, and backward-compatible 3.0 support. Includes the new `context` prompt for automatic user profile injection.
- **S3 Connector:** New connector to sync documents from Amazon S3 buckets, with console UI for bucket configuration.
- **Memory Graph Revamp:** Complete rewrite of `@supermemory/memory-graph` with improved visualization and performance.
## December 24, 2025
- **`@supermemory/tools` — Vercel AI SDK v5/v6:** Now supports both Vercel AI SDK v5 and v6, with automatic version detection.
- **Conversation Support in SDKs:** `supermemory` (TypeScript) and `supermemory-openai-sdk` (Python) now support the conversations API for multi-turn chat with memory.
- **MemoryBench:** New open-source benchmark suite for evaluating memory systems, with documentation and CLI.
## December 17, 2025
- **Hybrid Search Mode:** New `hybrid` search mode in `/v4/search` combining semantic and keyword search for better recall on technical queries.
## December 9, 2025
- **Firecrawl Integration:** Web crawling powered by Firecrawl for more reliable extraction of website content, with fallback support.
- **Custom GitHub Credentials:** Bring your own GitHub OAuth app credentials for the GitHub connector, enabling private repo access.
- **API Key Expiration Emails:** API keys now trigger email notifications before expiration.
- **Connector Sync Logs:** Connection syncs now produce detailed logs visible in the console.
## December 2, 2025
- **Organization Deletion:** Organizations can now be fully deleted from the console, including all associated data.
- **Billing Page Redesign:** New billing layout with invoicing support and improved usage visibility.
- **Console Onboarding Improvements:** Streamlined onboarding flow for new users.
## December 5, 2025
- **`@supermemory/tools` — Browser API Key Support:** `apiKey` can now be passed via options instead of relying on `process.env`, enabling browser-based usage of the tools package.
## November 17, 2025
- **Web Crawler Connector:** New connector to crawl and index entire websites with configurable depth and URL patterns.
- **`@supermemory/memory-graph` Package:** New package for building interactive graph visualizations of memory connections, with a standalone playground.
- **OpenAI Responses API Support:** `@supermemory/tools` OpenAI integration now supports the Responses API.
- **`supermemory-openai-sdk` — Python Middleware:** New `withSupermemory` middleware for the Python OpenAI SDK, enabling transparent memory injection into OpenAI API calls.
- **Browser Extension Webpage Capture:** Chrome extension can now capture full webpage content with markdown conversion, not just bookmarks.
- **Bulk Memory Optimization:** Memory creation now uses bulk inserts for significantly faster batch ingestion.
## October 27, 2025
- **Enhanced Filtering Capabilities:** Major improvements to the search filtering API with new `string_contains` filter type for partial string matching, `ignoreCase` option for case-insensitive string operations, and improved negation support across all filter types including proper numeric equality negation. The implementation also includes enhanced SQL injection protection and wildcard escaping for improved security.
## September 17, 2025
- **Forgotten Memories Search:** New `include.forgottenMemories` parameter in v4 search API allows searching through memories that have been explicitly forgotten or expired. Set to `true` to include forgotten memories in search results, helping recover previously archived information.
## September 14, 2025
- **Enhanced Delete API:** `DELETE /v3/documents/:id` endpoint now supports both internal document ID and customId for flexible document deletion. Developers can now delete documents using the same customId provided during creation, improving API consistency with other endpoints.
- **API Terminology Clarification:** Refined API terminology from "memories" to "documents" for improved developer clarity. New `/v3/documents/*` endpoints provide more intuitive naming while maintaining full backward compatibility via automatic redirects from `/v3/memories/*`. No action required from existing integrations.
## September 13, 2025
- **Documentation v2.0:** Complete rewrite with comprehensive API references, cookbook recipes, and production-ready examples for TypeScript, Python, and cURL
- **AI SDK Integration:** New `@supermemory/tools/ai-sdk` package for native Vercel AI SDK integration with memory tools and infinite chat capabilities
- **Bulk Delete Endpoint:** New `DELETE /v3/documents/bulk` endpoint for efficient memory management
## September 5, 2025
- **Memory Search Endpoint:** New `/v4/search` endpoint optimized for conversational AI and memory retrieval (vs document search)
- **Advanced Memory Management:** Enhanced update/delete operations with better filtering and batch processing capabilities
## August 30, 2025
- **MCP (Model Context Protocol) Server:** Launch of supermemory MCP server for AI model integrations with full project support and auto-detection
- **Enhanced Filtering API:** Improved SQL-based filtering with array_contains, numeric operators, and complex AND/OR logic
## August 15, 2025
- **Memory Router Proxy:** Enhanced proxy functionality for LLM requests with automatic context management and token optimization
- **Search Algorithm Updates:** Configurable similarity thresholds, reranking, and query rewriting for better result quality
## April 30, 2025
- **Comprehensive API Documentation:** New interactive API references with detailed parameter explanations and response schemas
- **Container Tags System:** Enhanced organizational grouping for better memory isolation and user-scoped content
- **Auto Content Type Detection:** Automatic processing of PDFs, images, videos, and web content regardless of URL extensions
## April 28, 2025
- **Google Drive Connector API:** New endpoints for programmatic Google Drive integration and file syncing
## April 25, 2025
- **Search Threshold Controls:** New `documentThreshold` and `chunkThreshold` parameters for fine-tuning search sensitivity
- **Document-Specific Search:** New `docId` parameter to search within specific large documents
- **Enhanced Chunk Control:** `onlyMatchingChunks` parameter for precise result filtering
## April 24, 2025
- **Query Rewriting API:** Automatic query expansion and intent matching for better search results
- **Search Context Options:** New `includeFullDocs` and `includeSummary` parameters for comprehensive document retrieval
## April 18, 2025
- **Enhanced Content Processing:** Improved ingestion pipeline supporting direct URL processing for images, videos, and PDFs
- **Stable Web Ingestion:** More reliable processing of website URLs with better content extraction
## April 14, 2025
- **Team API Endpoints:** New endpoints for team management and permission control
- **Enhanced Analytics API:** Better observability with detailed usage metrics and performance data
## February 1, 2025
- **Multi-Space Search:** Search across multiple container tags simultaneously with array parameter support
- **API Versioning:** Migration to `/v1` endpoints with improved versioning strategy
- **Interactive API Playground:** New testing interface for all endpoints with live examples

View file

@ -0,0 +1,779 @@
---
title: "Changelog"
sidebarTitle: "Supermemory"
description: "New updates and improvements to Supermemory"
---
<Update label="May 27, 2026" tags={["API"]}>
### Instant dreaming
New `dreaming` parameter on `POST /v3/documents` and `POST /v3/documents/batch`. Default `"dynamic"` groups related documents together so memories form from coherent, logical units. Set `"dreaming": "instant"` to process a single document on its own — bills one extra operation per document. Omit the parameter and behavior is unchanged.
</Update>
<Update label="April 13, 2026" tags={["Integrations", "API"]}>
### Google Drive: scoped sync by default
New Google Drive connections default to **folder and file** scope: after OAuth, users complete a hosted picker; only selected items sync. Set `metadata.syncScope` to `"full"` on connection creation to sync the entire Drive without the picker. Scoped connections without a saved selection are skipped by import jobs until setup is finished.
</Update>
<Update label="March 18, 2026" tags={["API", "SDK", "Console", "CLI"]}>
### Supermemory CLI
New command-line tool for managing memories, documents, profiles, tags, connectors, and API keys directly from the terminal.
### `@supermemory/tools` v1.4.1
Now exposes raw `searchResults` in `MemoryPromptData`, giving full control over how retrieved memories are formatted in prompts.
### PPTX & Audio Ingestion
PowerPoint files (`.pptx`) are now a supported content type. Audio files are automatically transcribed via Gemini 2.5 Flash, chunked, and indexed.
### Multi-containerTag Scoped API Keys
Scoped API keys can now be assigned to multiple container tags — one key, multiple spaces.
### Console: Documents Page
New dedicated documents browser in the console for viewing, filtering, and managing all ingested content.
</Update>
<Update label="March 9, 2026" tags={["API", "SDK", "MCP", "Integrations"]}>
### Delete Scoped API Keys
New `DELETE` endpoint to disable scoped API keys programmatically.
### `supermemory-agent-framework` Python Package
Official Python package for using Supermemory with Microsoft's Agent Framework — memory tools and middleware out of the box.
### Interactive Memory Graph in MCP
The MCP server now includes an interactive graph visualization app for exploring memory connections from any MCP-compatible client.
### More Integrations
- **ViaSocket** — new integration guide for automation workflows.
- **Plugin Auth Connect Page** — OAuth-style connect page for Claude Code, OpenCode, and OpenClaw.
- **OpenAI SDK Backfill** — improved compatibility across TypeScript and Python SDKs.
### Other
- **Retry failed documents** by re-submitting with the same `customId`.
- **Delete connection without documents** — disconnect a source without deleting synced content.
- **Org-level overage toggle** in billing settings.
- **Copyable team invite link** on the team management page.
- **`extends` relation type** in memory graph for richer knowledge graph connections.
- **Bulk delete** in the Nova app interface.
</Update>
<Update label="March 2, 2026" tags={["API"]}>
### Configurable Vector Stores
Bring your own vector store — Supermemory now supports pluggable vector backends beyond the default.
### List Memories Endpoint
New `GET /v3/documents` endpoint with pagination, filtering by container tag, status, and metadata.
</Update>
<Update label="February 26, 2026" tags={["API", "Console"]}>
### Self-Hostable Supermemory
Run the full Supermemory stack on your own infrastructure with Docker.
### Console v2
Complete redesign of the developer console with new navigation, improved billing, and a unified project view that merges consumer and developer organizations.
### No More 120 Memory Limit
The previous cap of 120 memories per container tag has been removed. Store unlimited memories.
</Update>
<Update label="February 22, 2026" tags={["API", "SDK", "CLI"]}>
### Supermemory Skill for Claude Code
Install with `npx skills add supermemoryai/skills` — teaches Claude to proactively recommend and implement Supermemory when building AI apps. Includes TypeScript and Python examples.
### API Improvements
- **Metadata filtering for profiles** — target profile queries by metadata fields.
- **List documents with multiple container tags** — new `operator` parameter.
- **Deprecate `include: chunks`** in `/v4/search` in favor of the `hybrid` search mode.
- **Content deduplication** in search results to reduce token usage.
</Update>
<Update label="February 9, 2026" tags={["API", "Console"]}>
### Unified Organizations
Consumer and developer organizations merged into a single org type. All orgs can now access both Nova and the developer API.
### Credits-Based Usage Display
Billing now shows token usage in a credits-based format.
### Nova Spaces with Multi-Select
Spaces in Nova support multi-select, replacing "All Spaces" with scoped "Nova Spaces."
</Update>
<Update label="February 6, 2026" tags={["API"]}>
### Scoped API Keys for Container Tags
Create API keys scoped to specific container tags for fine-grained access control per space.
### DELETE Endpoint for Container Tags
New endpoint to delete container tags and their associated document relationships.
### Container Tag-Level Context Prompts
Set custom context prompts per container tag to control how memories are extracted and summarized within each space.
</Update>
<Update label="February 3, 2026" tags={["Integrations", "SDK"]}>
### New Framework Integration Docs
Added guides for LangGraph, OpenAI Agents SDK, CrewAI, Agno, Mastra, LangChain, and Claude Code — covering all major AI agent frameworks.
### Entity Context & Authentication Docs
New docs on entity extraction, context enrichment, and comprehensive authentication examples (API key, OAuth, scoped keys).
</Update>
<Update label="January 25, 2026" tags={["API", "Console", "SDK"]}>
### Plugin Authentication System
New auth system for external tool integrations, enabling secure plugin-to-API connections. Dedicated plugin page with auth flows for Claude Code, OpenCode, and OpenClaw.
### Enterprise Plan Support
Enterprise tier now available in the console.
### `@supermemory/tools` — Strict Mode
Strict mode support for OpenAI function calling, ensuring schema-validated tool calls.
</Update>
<Update label="January 14, 2026" tags={["API"]}>
### Hybrid PDF Pipeline
PDF extraction now uses Mistral OCR 3 with Gemini fallback for significantly improved accuracy on scanned documents and complex layouts.
### Halfvec Embeddings
Embedding storage optimized with half-precision vectors, reducing storage costs while maintaining search quality.
### Spaces Creation with Emoji
Create and customize spaces with emoji identifiers in Nova.
</Update>
<Update label="January 8, 2026" tags={["API", "SDK", "Integrations"]}>
### Gmail Connector
New connector to sync Gmail threads into Supermemory. Threads are stored in R2 for reliable processing of large mailboxes.
### `supermemory-pipecat` Python Package
New SDK for Pipecat voice AI pipelines, including Gemini Live speech-to-speech support.
### `@supermemory/tools` — Prompt Templates
Customize how memory context is formatted in AI SDK integrations with the new `promptTemplate` option.
### Other
- **Container tag filters** in list and search endpoints.
- **Pagination improvements** across the console.
</Update>
<Update label="December 30, 2025" tags={["MCP", "SDK", "API"]}>
### MCP 4.0
Major MCP server update with session configuration, project-aware tools on every init, and backward-compatible 3.0 support. New `context` prompt for automatic user profile injection into AI conversations.
### S3 Connector
New connector to sync documents from Amazon S3 buckets, with console UI for bucket configuration.
### Memory Graph Revamp
Complete rewrite of `@supermemory/memory-graph` with improved visualization and performance.
</Update>
<Update label="December 24, 2025" tags={["SDK"]}>
### `@supermemory/tools` — AI SDK v5/v6
Now supports both Vercel AI SDK v5 and v6 with automatic version detection.
### Conversation Support in SDKs
`supermemory` (TypeScript) and `supermemory-openai-sdk` (Python) now support the conversations API for multi-turn chat with memory.
### MemoryBench
New open-source benchmark suite for evaluating memory systems, with documentation and CLI.
</Update>
<Update label="December 17, 2025" tags={["API"]}>
### Hybrid Search Mode
New `hybrid` search mode in `/v4/search` combining semantic and keyword search for better recall on technical queries.
</Update>
<Update label="December 9, 2025" tags={["API", "Console"]}>
### Firecrawl Integration
Web crawling powered by Firecrawl for more reliable extraction of website content, with fallback support.
### Custom GitHub Credentials
Bring your own GitHub OAuth app credentials for the GitHub connector, enabling private repo access.
### API Key Expiration Emails
API keys now trigger email notifications before expiration.
### Connector Sync Logs
Connection syncs now produce detailed logs visible in the console.
</Update>
<Update label="December 5, 2025" tags={["SDK"]}>
### `@supermemory/tools` — Browser API Key Support
`apiKey` can now be passed via options instead of relying on `process.env`, enabling browser-based usage.
</Update>
<Update label="December 2, 2025" tags={["Console"]}>
### Organization Deletion
Organizations can now be fully deleted from the console, including all associated data.
### Billing Page Redesign
New billing layout with invoicing support and improved usage visibility.
### Console Onboarding Improvements
Streamlined onboarding flow for new users.
</Update>
<Update label="November 17, 2025" tags={["API", "SDK"]}>
### Web Crawler Connector
New connector to crawl and index entire websites with configurable depth and URL patterns.
### `@supermemory/memory-graph` Package
New package for building interactive graph visualizations of memory connections, with a standalone playground.
### OpenAI Responses API Support
`@supermemory/tools` OpenAI integration now supports the Responses API.
### `supermemory-openai-sdk` — Python Middleware
New `withSupermemory` middleware for the Python OpenAI SDK, enabling transparent memory injection into OpenAI API calls.
### Browser Extension Webpage Capture
Chrome extension can now capture full webpage content with markdown conversion.
### Bulk Memory Optimization
Memory creation now uses bulk inserts for significantly faster batch ingestion.
</Update>
<Update label="October 27, 2025" tags={["API", "SDK"]}>
### Enhanced Filtering
New `string_contains` filter type for partial string matching, `ignoreCase` option for case-insensitive operations, and improved negation support. Enhanced SQL injection protection.
### `withSupermemory` for OpenAI SDK
New `withSupermemory` wrapper for the OpenAI TypeScript SDK — transparent memory injection with automatic assistant response capture.
### Zapier & n8n Integration Pages
New integration guides for connecting Supermemory with Zapier and n8n automation workflows.
</Update>
<Update label="October 10, 2025" tags={["SDK", "API", "Console"]}>
### `@supermemory/tools` — AI SDK `withSupermemory`
New `withSupermemory` language model wrapper for Vercel AI SDK that automatically injects memory context and captures assistant responses.
### Raycast Extension
New Raycast extension for quick memory access and addition from the macOS launcher.
### User Profiles API
New `/v4/profile` endpoint for retrieving AI-generated user profiles derived from memory interactions, with container tag scoping.
### Other
- **DOCX support** — Word documents can now be ingested.
- **Project selection for connectors** — assign Google Drive, Notion, and OneDrive connections to specific projects.
- **Multiple models in consumer chat** — model switcher with system prompt improvements.
- **Organization settings** — configure Supermemory behavior (chunking, extraction, memory limits) per org.
</Update>
<Update label="September 17, 2025" tags={["API", "Console"]}>
### Forgotten Memories Search
New `include.forgottenMemories` parameter in v4 search API to search through memories that have been explicitly forgotten or expired.
### Enhanced Delete API
`DELETE /v3/documents/:id` now supports both internal document ID and `customId`.
### API Terminology Update
Renamed "memories" to "documents" for developer clarity. New `/v3/documents/*` endpoints with full backward compatibility via automatic redirects from `/v3/memories/*`.
### Console Revamp
New console design with dark/light mode, org switcher, billing invoices, space selector with search, and memory list with multi-delete.
### Other
- **New filters** — revamped filtering UI in the console.
- **Onboarding redesign** — new step-based onboarding with code samples.
- **Configurable chunking** — set chunk size and algorithm per org.
</Update>
<Update label="September 12, 2025" tags={["API", "SDK"]}>
### Documentation v2.0
Complete rewrite with comprehensive API references, cookbook recipes, and production-ready examples for TypeScript, Python, and cURL.
### `@supermemory/tools` Package
New tools package for native Vercel AI SDK and OpenAI integration with memory tools and infinite chat. Plus `openai-python-sdk` for Python middleware.
### Batch Add & Bulk Delete
New `POST /v3/documents/batch` for batch ingestion and `DELETE /v3/documents/bulk` for bulk deletion.
### Memory Forgetfulness System
Full lifecycle management with `forgetAfter` dates and forgotten memory filtering.
### Video Uploads
Video files can now be ingested with automatic content extraction.
</Update>
<Update label="September 1, 2025" tags={["MCP", "Console"]}>
### MCP Connection Flow Redesign
Step-based UI for connecting MCP clients with v1 migration support. One-click install for Cursor.
### Claude.ai & t3.chat Extension Support
Browser extension now integrates directly with Claude.ai and t3.chat for automatic memory search during conversations.
### Waitlist Removed
Supermemory is now open to all users — no more waitlist.
</Update>
<Update label="August 24, 2025" tags={["API", "Console"]}>
### New Landing Page & Developer Page
Redesigned marketing pages with developer-focused content, SEO improvements, and mobile responsiveness.
### Direct Webpage Ingestion
Ingest web content with `<sm-highlight>` tags for targeted extraction.
### Usage Limits Dashboard
Billing usage and limits now visible directly in the console dashboard.
### Other
- **Allow all CORS origins** for easier API integration.
- **Single `containerTag` in add memory** — simpler API for basic use cases.
- **Improved MCP project handling** — better project scoping in the MCP server.
</Update>
<Update label="August 16, 2025" tags={["Console"]}>
### New Consumer App
Complete rewrite of the consumer-facing app — new chat experience with slide-out window, masonry memory grid with infinite scroll, PWA support, and mobile-responsive menu bar.
### Memory Graph with WebGL
Graph rendering now uses WebGL for smooth visualization of thousands of memory connections. Search highlights relevant nodes with zoom.
### Chat Rewrite
New chat system with memory-aware conversations, regeneration, copy buttons, and the ability to add memories through chat.
### Dynamic Node Relations
Memory graph now supports `update`, `extend`, and `derive` relation types. Memories can be inferred from multiple parent documents.
</Update>
<Update label="August 12, 2025" tags={["API"]}>
### PDF Support for Google Drive
Google Drive connector now processes PDF files alongside Docs, Sheets, and Slides.
### Encrypted Connector Credentials
Google Drive, OneDrive, and Notion client secrets are now encrypted at rest.
### Bulk Memory Delete
New endpoint for deleting multiple memories at once.
### Self-Host Support
Initial self-hosting support — run Supermemory on your own infrastructure.
</Update>
<Update label="August 1, 2025" tags={["Console", "API"]}>
### Console Migrated to Cloudflare
Console app moved from Vercel to Cloudflare Workers for improved performance and lower latency.
### Autumn Payments Integration
Billing system integrated with Autumn for subscription management, waitlist early access, and usage tracking.
### New Developer Dashboard
Redesigned developer dashboard with API key display in code snippets, limits visualization, and MCP installation instructions.
</Update>
<Update label="July 25, 2025" tags={["Console", "MCP"]}>
### Consumer App v0
First version of the consumer app with chat, memory browsing, project management, and profile view. New consumer-oriented landing page.
### MCP → Agents SDK
MCP server migrated to the Agents SDK architecture for better reliability and project support.
### New Billing
Revamped billing page with upgrade buttons and plan management.
</Update>
<Update label="July 16, 2025" tags={["Console", "API"]}>
### Memory Graph Rewrite
Complete rewrite of the graph visualization — faster rendering, better layout, and interactive exploration.
### Onboarding
New guided onboarding flow for first-time console users.
### Notion Webhooks
Real-time sync for Notion connections via webhook integration.
</Update>
<Update label="July 5, 2025" tags={["Console"]}>
### Landing Page Rewrite
New marketing site with glass UI design, rewritten pricing page, and dedicated MCP page.
### Billing Page
New billing page with upgrade buttons and plan comparison.
### PostHog Analytics
Analytics tracking added across the console and landing page.
</Update>
<Update label="June 21, 2025" tags={["API"]}>
### OneDrive Connector
New connector for syncing OneDrive files with webhook-based real-time updates.
### Connectors BYOK
Bring your own API keys for connector integrations (Google Drive, OneDrive, Notion).
### Google Sheets & Slides
Google Drive connector now supports Sheets and Slides alongside Docs.
</Update>
<Update label="June 12, 2025" tags={["Console", "API"]}>
### Console Dashboard
First version of the dashboard overview page with memory analytics, container tag distribution charts, and usage metrics.
### Google Drive Webhooks
Real-time sync — Google Drive changes are automatically detected and processed.
### Sentry Integration
Error monitoring added across the console and API.
</Update>
<Update label="May 28, 2025" tags={["API", "Console"]}>
### Launch-Ready API
Console reached launchable state with login page improvements, auth fixes, and the first version of the new dashboard with React Query.
### Infinite Chat
Memory Router proxy with automatic context compression for infinite-length conversations with LLMs.
### Container Tags in Search
Filter search results by container tags for scoped memory retrieval.
### Google Docs MD Export
Google Drive connector switched from PDF to Markdown export for better content fidelity.
</Update>
<Update label="May 8, 2025" tags={["API"]}>
### API v3
New `/v3/` endpoints replacing v2 — cleaner routes, updated memory endpoint, and new update/delete operations.
### OneDrive Connector
Initial OneDrive integration for syncing files into Supermemory.
### Connections Architecture
New connection-document relationship model for tracking which connector synced which document.
</Update>
<Update label="April 30, 2025" tags={["API"]}>
### Comprehensive API Documentation
New interactive API references on Mintlify with detailed parameter explanations, response schemas, and bearer auth.
### Container Tags System
Enhanced organizational grouping for better memory isolation and user-scoped content.
### Auto Content Type Detection
Automatic processing of PDFs, images, videos, and web content regardless of URL extensions.
</Update>
<Update label="April 28, 2025" tags={["API"]}>
### Google Drive Connector
New endpoints for programmatic Google Drive integration and file syncing.
</Update>
<Update label="April 25, 2025" tags={["API"]}>
### Search Improvements
- **`documentThreshold` and `chunkThreshold`** — fine-tune search sensitivity.
- **`docId` parameter** — search within specific large documents.
- **`onlyMatchingChunks`** — precise result filtering.
- **`endUserId` filtering** — scope search to specific users.
- **Reranking** — improved result quality with a reranking step.
</Update>
<Update label="April 22, 2025" tags={["API", "MCP"]}>
### Supermemory MCP Server
First version of the MCP server for AI model integrations.
### Personalisation
AI-generated personalization based on user memory patterns.
### List Memories Endpoint
First version of the list memories API with pagination.
</Update>
<Update label="April 14, 2025" tags={["API"]}>
### Team API
Organization invites and user management endpoints.
### Analytics API
Hourly analytics tracking with detailed usage metrics.
### Content Processing Pipeline
New ingestion workflow with status tracking: `queued` → `extracting` → `chunking` → `embedding` → `done`.
</Update>
<Update label="March 27, 2025" tags={["API"]}>
### Connections System
First version of the connectors architecture — sync external data sources into Supermemory.
### Tag-Based Filtering
Filter memories by tags in search and list operations.
### Advanced Analytics
Request tracking, error counts, and usage metrics per organization.
</Update>
<Update label="March 18, 2025" tags={["API"]}>
### Supermemory API v2
The platform begins — Cloudflare Workers API with auth, ingestion workflows, vector search, and organization support. Built on Hono, Drizzle ORM, and Cloudflare D1/Hyperdrive.
</Update>
<Update label="January 20, 2025" tags={["Console"]}>
### Supermemory v2 Release
Major release of the consumer web app with new import tools (CSV, Markdown/Obsidian), improved hybrid search with date relevancy, batch delete, and space management (edit/delete names).
### Docs Site Launch
First version of the documentation site with API reference, getting started guide, and pricing page.
</Update>
<Update label="August 16, 2024" tags={["Console"]}>
### Supermemory v1 — Major Update
New consumer app version with canvas/note editor, text-to-speech on AI answers, PWA support, improved Telegram bot with Markdown, and memory queue processing. Extension gets drag-and-dismiss features.
</Update>
<Update label="July 21, 2024" tags={["Console"]}>
### ProductHunt Launch
Supermemory launches on ProductHunt. Features at launch: shareable spaces, Twitter thread import, AI chat with citations, onboarding flow, recommended items, chat history, and keyboard shortcuts.
</Update>
<Update label="June 23, 2024" tags={["Console"]}>
### Multi-Turn Chat & Canvas
Added multi-turn conversations, canvas with drag-and-drop, Telegram bot, vector lookup 2x speedup, and the first version of the Chrome extension.
</Update>
<Update label="May 18, 2024" tags={["API"]}>
### Backend Rewrite to Hono
Backend migrated from Next.js API routes to Hono on Cloudflare Workers. Landing page redesign, browser rendering for web content extraction.
</Update>
<Update label="April 11, 2024" tags={["Console"]}>
### Supermemory v1 Launch
First public release — spaces, chat with AI, Twitter bookmarks import, Chrome extension with save-from-page, notes editor, and search across all saved content.
</Update>
<Update label="February 21, 2024" tags={["Console"]}>
### Supermemory is Born
Initial monorepo setup with auth, Chrome extension, AI chat with citations using OpenAI embeddings, and the first version of the web app.
</Update>

View file

@ -0,0 +1,81 @@
---
title: "Plugin changelog"
sidebarTitle: "Plugins"
description: "Recent updates and improvements to Supermemory plugins"
---
<Update label="June 20, 2026" tags={["OpenCode", "Cursor"]}>
### OpenCode entity context
OpenCode now sends entity context with memory operations, so saved context can stay tied to the active project and conversation. The entity-context prompt was also moved out of the API client for cleaner reuse across capture and compaction flows.
### Cursor session auth
Cursor now starts the auth flow from the session hook when needed, and the OAuth success screen uses the Cursor-branded callback path.
</Update>
<Update label="June 18, 2026" tags={["Claude Code", "OpenCode"]}>
### Claude Code update notices
Claude Code now surfaces plugin update notices during sessions and includes the latest package/version metadata.
### OpenCode context prompt
OpenCode gained an entity-context prompt so memory recall and capture can carry more precise source context.
</Update>
<Update label="June 13, 2026" tags={["Claude Code", "Codex"]}>
### Claude Code marketplace polish
The Claude Code plugin manifest was polished for the official marketplace listing, including refreshed metadata and naming.
### Codex update notices
Codex now checks for plugin updates during session start and shows a user-visible notice when a newer version is available.
</Update>
<Update label="June 11, 2026" tags={["Claude Code", "Cursor"]}>
### Claude Code rename migration
Claude Code completed the rename to the `supermemory` plugin while keeping migration safe for users already on the new plugin name. Configuration also supports custom `baseUrl` values for local or self-hosted Supermemory installs.
### Cursor web OAuth
Cursor OAuth now routes through the Supermemory web app, keeping the plugin auth flow consistent with the rest of the integrations.
</Update>
<Update label="June 10, 2026" tags={["Codex", "OpenCode"]}>
### Codex auth and status tooling
Codex added status, logout, and web-auth flows, plus Windows-safe auth URL opening and entity context for saved memories. The installer now includes a `supermemory-status` skill so Codex can report connection, hook, config, and installed-skill health from inside a session.
### OAuth status refinements
Codex and OpenCode integration status now renders more clearly in the Supermemory app during OAuth connection and setup.
</Update>
<Update label="June 6, 2026" tags={["Claude Code", "Cursor", "OpenClaw", "Hermes"]}>
### Claude Code recall reasoning
Claude Code gained reasoned per-turn memory recall with auto-approve support, refreshed bundled scripts, and updated skill names for `supermemory-save` and `supermemory-search`.
### Cursor session hooks
Cursor session hooks now load reliably and persist real project sessions into the correct container.
### OpenClaw and Hermes memory attribution
Saved plugin memories now parse source attribution more accurately, and the dashboard shows the correct plugin logos and recent-memory rows for OpenClaw and Hermes.
</Update>

View file

@ -1,6 +1,6 @@
--- ---
title: "Container Tags" title: "Container Tags"
sidebarTitle: "Container tags" sidebarTitle: "Container Tags"
description: "The isolation boundary that groups and partitions memories by user, project, or any logical scope" description: "The isolation boundary that groups and partitions memories by user, project, or any logical scope"
icon: "folder" icon: "folder"
--- ---
@ -32,7 +32,7 @@ await client.add({
}); });
// Later, retrieve only Alex's memories // Later, retrieve only Alex's memories
const results = await client.search({ const results = await client.search.memories({
q: "what are the user's UI preferences?", q: "what are the user's UI preferences?",
containerTag: "user_alex", containerTag: "user_alex",
}); });
@ -103,7 +103,7 @@ The same tag flows through the entire lifecycle of a memory. Pass it consistentl
await client.add({ content: "Q1 planning notes", containerTag: "project_q1" }); await client.add({ content: "Q1 planning notes", containerTag: "project_q1" });
// Search within the same container // Search within the same container
await client.search({ q: "planning", containerTag: "project_q1" }); await client.search.memories({ q: "planning", containerTag: "project_q1" });
// List everything in the container // List everything in the container
await client.documents.list({ containerTags: ["project_q1"] }); await client.documents.list({ containerTags: ["project_q1"] });
@ -170,10 +170,7 @@ Keep tags **deterministic** — derive them directly from IDs you already have (
<Card title="Organizing & Filtering" icon="filter" href="/concepts/filtering"> <Card title="Organizing & Filtering" icon="filter" href="/concepts/filtering">
Combine container tags with metadata filters for precise retrieval. Combine container tags with metadata filters for precise retrieval.
</Card> </Card>
<Card title="Scoped API keys" icon="key" href="/authentication#scoped-api-keys"> <Card title="Adding Memories" icon="plus" href="/add-memories">
Mint keys that can only touch one container — multi-tenant clients without the org master key.
</Card>
<Card title="Adding Memories" icon="plus" href="/ingestion/add-memories">
See container tags in action across the add API. See container tags in action across the add API.
</Card> </Card>
</CardGroup> </CardGroup>

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