Compare commits
119 commits
server-v0.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5258cb74c8 | ||
|
|
4d8a4ebfdd | ||
|
|
9a0c5a5ad6 | ||
|
|
17eab43cd4 | ||
|
|
4ad5f0beb1 | ||
|
|
03773c4f2e | ||
|
|
d0f53b0d64 | ||
|
|
b01d2b69a3 | ||
|
|
7974498062 | ||
|
|
4173edb9e6 | ||
|
|
c262cc9953 | ||
|
|
879ddd5c95 | ||
|
|
46d1b53230 | ||
|
|
de3bbb3ce9 | ||
|
|
5fee2f2872 | ||
|
|
ece20ff53e | ||
|
|
348483d5e8 | ||
|
|
c5b7e7d4fc | ||
|
|
143024fa34 | ||
|
|
9ccd1b64c3 | ||
|
|
d436792e77 | ||
|
|
29c43984fe | ||
|
|
f11d8c4620 | ||
|
|
9652478093 | ||
|
|
3f7b9667c6 | ||
|
|
e4afc770be | ||
|
|
f051af098e | ||
|
|
6cae175852 | ||
|
|
3b0fc9c959 | ||
|
|
3487666481 | ||
|
|
dda56e766e | ||
|
|
818a83a381 | ||
|
|
7b1175cb1a | ||
|
|
20410a6862 | ||
|
|
7d59070ad6 | ||
|
|
18a2dfbe39 | ||
|
|
149589ae7e | ||
|
|
c0eb81c887 | ||
|
|
e2be9c9edd | ||
|
|
5d2b5855fe | ||
|
|
d14b209f7c | ||
|
|
e651045ac5 | ||
|
|
5ecbc26345 | ||
|
|
9cbddcec56 | ||
|
|
2e85722cf4 | ||
|
|
eac070048b | ||
|
|
82dae50ef4 | ||
|
|
7f448d55d8 | ||
|
|
9d64e0f950 | ||
|
|
1356affbd1 | ||
|
|
fcf49855ce | ||
|
|
0695ca421b | ||
|
|
c70c142fc7 | ||
|
|
b7a6ea9a5f | ||
|
|
00e57fb9c2 | ||
|
|
47152afc1d | ||
|
|
14bcc92c31 | ||
|
|
74b2201eeb | ||
|
|
f163c932cf | ||
|
|
a7efd817ec | ||
|
|
59b148e5b2 | ||
|
|
be267c2fc8 | ||
|
|
2731de5c06 | ||
|
|
45585b4c0f | ||
|
|
bf81cb94ea | ||
|
|
069b8c373c | ||
|
|
73d15ac9f8 | ||
|
|
570ed22b6c | ||
|
|
4d86b728dd | ||
|
|
a99cf4f7e1 | ||
|
|
434c86e2c3 | ||
|
|
05aab3a09f | ||
|
|
a787041ca7 | ||
|
|
f14cdd7a4c | ||
|
|
af9c6b74e8 | ||
|
|
219cb64c82 | ||
|
|
785c96e682 | ||
|
|
7e7e820489 | ||
|
|
68769200b0 | ||
|
|
48fb1969f8 | ||
|
|
9e194fbc50 | ||
|
|
e8ed80f768 | ||
|
|
b21ea31ffc | ||
|
|
a7161c7132 | ||
|
|
ff40a82d69 | ||
|
|
04e5b4dccf | ||
|
|
19e8f06cf1 | ||
|
|
d4377b1609 | ||
|
|
95a34602b7 | ||
|
|
a051ba0e28 | ||
|
|
33e927417f | ||
|
|
1034e337ba | ||
|
|
8071a7b085 | ||
|
|
5fa0535a64 | ||
|
|
db7f5c3f64 | ||
|
|
ac880a4dc6 | ||
|
|
8a352dca81 | ||
|
|
fa7588c43e | ||
|
|
7e182fca9a | ||
|
|
6f3c835e8f | ||
|
|
80af8c9043 | ||
|
|
c7dc49decf | ||
|
|
4aa044fe55 | ||
|
|
e685762012 | ||
|
|
ea2cf33fd3 | ||
|
|
cfc2b49192 | ||
|
|
5a3ff85ea5 | ||
|
|
3c3accaab9 | ||
|
|
72431e9afc | ||
|
|
d5f95827ed | ||
|
|
fa16e853a8 | ||
|
|
90f709bf8d | ||
|
|
fa0823a300 | ||
|
|
f882f1104d | ||
|
|
816b85d756 | ||
|
|
9bbe436fab | ||
|
|
e39ba92cb4 | ||
|
|
2426305a2d | ||
|
|
86c3ad69d1 |
257
.github/workflows/ci-python.yml
vendored
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
name: CI - Python SDKs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "packages/agent-framework-python/**"
|
||||||
|
- "packages/cartesia-sdk-python/**"
|
||||||
|
- "packages/openai-sdk-python/**"
|
||||||
|
- "packages/pipecat-sdk-python/**"
|
||||||
|
- ".github/workflows/ci-python.yml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
agent-framework-python:
|
||||||
|
name: agent-framework-python (${{ matrix.dependency-lane }}, Python ${{ matrix.python-version }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- python-version: "3.10"
|
||||||
|
dependency-lane: minimum-supermemory
|
||||||
|
supermemory-version: "3.16.0"
|
||||||
|
- python-version: "3.13"
|
||||||
|
dependency-lane: current-supermemory
|
||||||
|
supermemory-version: "3.59.0"
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: packages/agent-framework-python
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: packages/agent-framework-python/pyproject.toml
|
||||||
|
|
||||||
|
- name: Install build and test tools
|
||||||
|
run: python -m pip install build pytest pytest-asyncio
|
||||||
|
|
||||||
|
- name: Build wheel
|
||||||
|
run: python -m build --wheel --outdir "$RUNNER_TEMP/wheels"
|
||||||
|
|
||||||
|
- name: Install wheel and tested Supermemory SDK
|
||||||
|
run: >-
|
||||||
|
python -m pip install "$RUNNER_TEMP"/wheels/*.whl
|
||||||
|
"supermemory==${{ matrix.supermemory-version }}"
|
||||||
|
|
||||||
|
- name: Check dependency compatibility
|
||||||
|
run: python -m pip check
|
||||||
|
|
||||||
|
- name: Verify installed wheel and SDK version
|
||||||
|
run: >-
|
||||||
|
python -c "from importlib.metadata import version; from pathlib import Path;
|
||||||
|
import supermemory_agent_framework;
|
||||||
|
assert version('supermemory') == '${{ matrix.supermemory-version }}';
|
||||||
|
assert 'site-packages' in Path(supermemory_agent_framework.__file__).parts"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: python -m pytest
|
||||||
|
|
||||||
|
openai-sdk-python:
|
||||||
|
name: openai-sdk-python (${{ matrix.dependency-lane }}, Python ${{ matrix.python-version }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- python-version: "3.9"
|
||||||
|
dependency-lane: minimum-supermemory
|
||||||
|
supermemory-version: "3.50.0"
|
||||||
|
expected-supermemory-version: "3.50.0"
|
||||||
|
- python-version: "3.12"
|
||||||
|
dependency-lane: locked
|
||||||
|
supermemory-version: ""
|
||||||
|
expected-supermemory-version: "3.59.0"
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: packages/openai-sdk-python
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Setup uv
|
||||||
|
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||||
|
with:
|
||||||
|
version: "0.12.5"
|
||||||
|
enable-cache: true
|
||||||
|
working-directory: packages/openai-sdk-python
|
||||||
|
cache-dependency-glob: uv.lock
|
||||||
|
|
||||||
|
- name: Install locked dependencies
|
||||||
|
run: uv sync --locked --python "${{ matrix.python-version }}"
|
||||||
|
|
||||||
|
- name: Build wheel
|
||||||
|
run: uv build --wheel --out-dir "$RUNNER_TEMP/wheels"
|
||||||
|
|
||||||
|
- name: Install built wheel
|
||||||
|
run: >-
|
||||||
|
uv pip install --python .venv/bin/python --reinstall --no-deps
|
||||||
|
"$RUNNER_TEMP"/wheels/*.whl
|
||||||
|
|
||||||
|
- name: Install minimum Supermemory SDK
|
||||||
|
if: matrix.supermemory-version != ''
|
||||||
|
run: >-
|
||||||
|
uv pip install --python .venv/bin/python
|
||||||
|
"supermemory==${{ matrix.supermemory-version }}"
|
||||||
|
|
||||||
|
- name: Check dependency compatibility
|
||||||
|
run: uv pip check --python .venv/bin/python
|
||||||
|
|
||||||
|
- name: Verify installed wheel and SDK version
|
||||||
|
run: >-
|
||||||
|
.venv/bin/python -c "from importlib.metadata import version;
|
||||||
|
from pathlib import Path; import supermemory_openai;
|
||||||
|
assert version('supermemory') == '${{ matrix.expected-supermemory-version }}';
|
||||||
|
assert 'site-packages' in Path(supermemory_openai.__file__).parts"
|
||||||
|
|
||||||
|
- name: Run tests without changing the verified environment
|
||||||
|
run: .venv/bin/python -m pytest
|
||||||
|
|
||||||
|
cartesia-sdk-python:
|
||||||
|
name: cartesia-sdk-python (${{ matrix.dependency-lane }}, Python ${{ matrix.python-version }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- python-version: "3.10"
|
||||||
|
dependency-lane: minimum-dependencies
|
||||||
|
supermemory-version: "3.16.0"
|
||||||
|
cartesia-line-version: "0.2.0"
|
||||||
|
- python-version: "3.12"
|
||||||
|
dependency-lane: current-dependencies
|
||||||
|
supermemory-version: "3.59.0"
|
||||||
|
cartesia-line-version: "0.2.17"
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: packages/cartesia-sdk-python
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: packages/cartesia-sdk-python/pyproject.toml
|
||||||
|
|
||||||
|
- name: Install build and test tools
|
||||||
|
run: python -m pip install build pytest
|
||||||
|
|
||||||
|
- name: Build wheel
|
||||||
|
run: python -m build --wheel --outdir "$RUNNER_TEMP/wheels"
|
||||||
|
|
||||||
|
- name: Install wheel and tested runtime dependencies
|
||||||
|
run: >-
|
||||||
|
python -m pip install "$RUNNER_TEMP"/wheels/*.whl
|
||||||
|
"supermemory==${{ matrix.supermemory-version }}"
|
||||||
|
"cartesia-line==${{ matrix.cartesia-line-version }}"
|
||||||
|
|
||||||
|
- name: Check dependency compatibility
|
||||||
|
run: python -m pip check
|
||||||
|
|
||||||
|
- name: Verify real Cartesia Line integration and run tests
|
||||||
|
run: >-
|
||||||
|
python -c "from importlib.metadata import version;
|
||||||
|
from pathlib import Path; import line, pytest, supermemory_cartesia;
|
||||||
|
assert version('supermemory') == '${{ matrix.supermemory-version }}';
|
||||||
|
assert version('cartesia-line') == '${{ matrix.cartesia-line-version }}';
|
||||||
|
assert 'site-packages' in Path(supermemory_cartesia.__file__).parts;
|
||||||
|
result = pytest.main(['-W', 'ignore::pytest.PytestAssertRewriteWarning', 'tests']);
|
||||||
|
raise SystemExit(result)"
|
||||||
|
|
||||||
|
pipecat-sdk-python:
|
||||||
|
name: pipecat-sdk-python (${{ matrix.dependency-lane }}, Python ${{ matrix.python-version }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- python-version: "3.10"
|
||||||
|
dependency-lane: minimum-dependencies
|
||||||
|
supermemory-version: "3.16.0"
|
||||||
|
pipecat-version: "0.0.98"
|
||||||
|
- python-version: "3.12"
|
||||||
|
dependency-lane: current-dependencies
|
||||||
|
supermemory-version: "3.59.0"
|
||||||
|
pipecat-version: "1.7.0"
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: packages/pipecat-sdk-python
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: packages/pipecat-sdk-python/pyproject.toml
|
||||||
|
|
||||||
|
- name: Install build and test tools
|
||||||
|
run: python -m pip install build pytest
|
||||||
|
|
||||||
|
- name: Build wheel
|
||||||
|
run: python -m build --wheel --outdir "$RUNNER_TEMP/wheels"
|
||||||
|
|
||||||
|
- name: Install wheel and tested runtime dependencies
|
||||||
|
run: >-
|
||||||
|
python -m pip install "$RUNNER_TEMP"/wheels/*.whl
|
||||||
|
"supermemory==${{ matrix.supermemory-version }}"
|
||||||
|
"pipecat-ai==${{ matrix.pipecat-version }}"
|
||||||
|
|
||||||
|
- name: Check dependency compatibility
|
||||||
|
run: python -m pip check
|
||||||
|
|
||||||
|
- name: Verify real Pipecat integration and run tests
|
||||||
|
run: >-
|
||||||
|
python -c "from importlib.metadata import version;
|
||||||
|
from pathlib import Path; import pipecat, pytest, supermemory_pipecat;
|
||||||
|
assert version('supermemory') == '${{ matrix.supermemory-version }}';
|
||||||
|
assert version('pipecat-ai') == '${{ matrix.pipecat-version }}';
|
||||||
|
assert 'site-packages' in Path(supermemory_pipecat.__file__).parts;
|
||||||
|
result = pytest.main(['-W', 'ignore::pytest.PytestAssertRewriteWarning', 'tests']);
|
||||||
|
raise SystemExit(result)"
|
||||||
79
.github/workflows/ci.yml
vendored
|
|
@ -26,8 +26,83 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run TypeScript type checking
|
- name: Detect SDK and playground changes
|
||||||
run: bunx turbo run check-types --filter='@supermemory/ai-sdk' --filter='@supermemory/memory-graph'
|
id: sdk-changes
|
||||||
|
run: |
|
||||||
|
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/tools; then
|
||||||
|
echo "tools=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "tools=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/ai-sdk; then
|
||||||
|
echo "ai_sdk=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "ai_sdk=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- apps/sdk-playground; then
|
||||||
|
echo "sdk_playground=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "sdk_playground=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Python for SDK Playground
|
||||||
|
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||||
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Setup uv for SDK Playground
|
||||||
|
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||||
|
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||||
|
with:
|
||||||
|
version: "0.12.5"
|
||||||
|
enable-cache: true
|
||||||
|
working-directory: apps/sdk-playground/python
|
||||||
|
cache-dependency-glob: uv.lock
|
||||||
|
|
||||||
|
- name: Validate SDK Playground Python server
|
||||||
|
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||||
|
working-directory: apps/sdk-playground/python
|
||||||
|
run: |
|
||||||
|
uv sync --locked --python 3.12
|
||||||
|
.venv/bin/python -m py_compile server.py
|
||||||
|
.venv/bin/python -c "import server"
|
||||||
|
|
||||||
|
- name: Run Tools unit tests
|
||||||
|
if: steps.sdk-changes.outputs.tools == 'true'
|
||||||
|
run: bun run --cwd packages/tools test:unit
|
||||||
|
|
||||||
|
- name: Build Tools package
|
||||||
|
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||||
|
run: bun run --cwd packages/tools build
|
||||||
|
|
||||||
|
- name: Run AI SDK type checking
|
||||||
|
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||||
|
run: bun run --cwd packages/ai-sdk check-types
|
||||||
|
|
||||||
|
- name: Run AI SDK unit tests
|
||||||
|
if: steps.sdk-changes.outputs.ai_sdk == 'true'
|
||||||
|
run: bun run --cwd packages/ai-sdk test:unit
|
||||||
|
|
||||||
|
- name: Build AI SDK package
|
||||||
|
if: steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||||
|
run: bun run --cwd packages/ai-sdk build
|
||||||
|
|
||||||
|
- name: Run SDK Playground type checking
|
||||||
|
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||||
|
run: bun run --cwd apps/sdk-playground check-types:app
|
||||||
|
|
||||||
|
- name: Build SDK Playground
|
||||||
|
if: steps.sdk-changes.outputs.sdk_playground == 'true'
|
||||||
|
run: bun run --cwd apps/sdk-playground build:app
|
||||||
|
|
||||||
|
- name: Run Memory Graph type checking
|
||||||
|
run: bun run --cwd packages/memory-graph check-types
|
||||||
|
|
||||||
|
- name: Run Memory Graph unit tests
|
||||||
|
run: bun run --cwd packages/memory-graph test
|
||||||
|
|
||||||
- name: Run Biome CI (format & lint on changed files)
|
- 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
|
||||||
|
|
|
||||||
17
.github/workflows/claude-auto-fix-ci.yml
vendored
|
|
@ -77,8 +77,6 @@ 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
|
||||||
|
|
@ -87,21 +85,8 @@ 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__supermemory,mcp__github"
|
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__github"
|
||||||
--mcp-config '{
|
|
||||||
"mcpServers": {
|
|
||||||
"supermemory": {
|
|
||||||
"type": "http",
|
|
||||||
"url": "https://mcp.supermemory.ai/mcp",
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer ${{ secrets.SUPERMEMORY_API_KEY }}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
|
|
|
||||||
14
.github/workflows/claude-code-review.yml
vendored
|
|
@ -38,6 +38,7 @@ 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
|
||||||
|
|
@ -48,18 +49,7 @@ 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__supermemory__*,mcp__github__*"
|
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,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.
|
||||||
|
|
|
||||||
13
.github/workflows/claude.yml
vendored
|
|
@ -67,15 +67,4 @@ 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__supermemory,mcp__github"
|
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__github"
|
||||||
--mcp-config '{
|
|
||||||
"mcpServers": {
|
|
||||||
"supermemory": {
|
|
||||||
"type": "http",
|
|
||||||
"url": "https://mcp.supermemory.ai/mcp",
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer ${{ secrets.SUPERMEMORY_API_KEY }}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
|
|
|
||||||
|
|
@ -23,20 +23,22 @@ jobs:
|
||||||
working-directory: ./packages/agent-framework-python
|
working-directory: ./packages/agent-framework-python
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: pip install hatchling build
|
run: python -m 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@release/v1
|
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||||
with:
|
with:
|
||||||
packages-dir: packages/agent-framework-python/dist/
|
packages-dir: packages/agent-framework-python/dist/
|
||||||
|
|
|
||||||
49
.github/workflows/publish-ai-sdk.yml
vendored
|
|
@ -38,26 +38,65 @@ jobs:
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
working-directory: .
|
||||||
|
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)
|
||||||
NPM_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "0.0.0")
|
if npm view "$PACKAGE_NAME@$LOCAL_VERSION" version >/dev/null 2>&1; then
|
||||||
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 (npm has $NPM_VERSION)"
|
echo "Publishing $LOCAL_VERSION."
|
||||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build
|
- name: Wait for the Tools dependency
|
||||||
|
if: steps.version-check.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
TOOLS_SPEC=$(jq -r '.dependencies["@supermemory/tools"]' package.json)
|
||||||
|
TOOLS_VERSION=${TOOLS_SPEC#^}
|
||||||
|
TOOLS_VERSION=${TOOLS_VERSION#~}
|
||||||
|
|
||||||
|
if [[ ! "$TOOLS_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
echo "Unsupported @supermemory/tools dependency spec: $TOOLS_SPEC" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for attempt in {1..20}; do
|
||||||
|
PUBLISHED_VERSION=$(npm view "@supermemory/tools@$TOOLS_VERSION" version 2>/dev/null || true)
|
||||||
|
if [ "$PUBLISHED_VERSION" = "$TOOLS_VERSION" ]; then
|
||||||
|
echo "@supermemory/tools@$TOOLS_VERSION is available on npm."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Waiting for @supermemory/tools@$TOOLS_VERSION (attempt $attempt/20)."
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "@supermemory/tools@$TOOLS_VERSION was not published within five minutes." >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Build Tools dependency
|
||||||
|
if: steps.version-check.outputs.changed == 'true'
|
||||||
|
run: bun run --cwd ../tools build
|
||||||
|
|
||||||
|
- name: Build AI SDK package
|
||||||
if: steps.version-check.outputs.changed == 'true'
|
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
|
||||||
|
|
|
||||||
|
|
@ -23,20 +23,22 @@ jobs:
|
||||||
working-directory: ./packages/cartesia-sdk-python
|
working-directory: ./packages/cartesia-sdk-python
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: pip install hatchling build
|
run: python -m 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@release/v1
|
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||||
with:
|
with:
|
||||||
packages-dir: packages/cartesia-sdk-python/dist/
|
packages-dir: packages/cartesia-sdk-python/dist/
|
||||||
|
|
|
||||||
10
.github/workflows/publish-pipecat-sdk-python.yml
vendored
|
|
@ -23,20 +23,22 @@ jobs:
|
||||||
working-directory: ./packages/pipecat-sdk-python
|
working-directory: ./packages/pipecat-sdk-python
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: pip install hatchling build
|
run: python -m 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@release/v1
|
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||||
with:
|
with:
|
||||||
packages-dir: packages/pipecat-sdk-python/dist/
|
packages-dir: packages/pipecat-sdk-python/dist/
|
||||||
|
|
|
||||||
65
README.md
|
|
@ -7,7 +7,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>State-of-the-art memory and context engine for AI. And yes - you can use it as a company/personal brain.</strong>
|
<strong>State-of-the-art memory and context engine for AI.</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>
|
||||||
|
|
||||||
Build your own personal supermemory by using our app. Builds **persistent memory graph across every conversation**.
|
Give Claude Code, Cursor, Codex and OpenCode **persistent memory across every conversation** with a plugin or the MCP server.
|
||||||
|
|
||||||
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,21 +107,11 @@ curl -fsSL https://supermemory.ai/install | bash
|
||||||
|
|
||||||
## Give your AI memory
|
## Give your AI memory
|
||||||
|
|
||||||
The Supermemory App, browser extension, plugins and MCP server gives any compatible AI assistant persistent memory. One install, and your AI remembers you.
|
Plugins and the MCP server give 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, OpenCode, OpenClaw, and Hermes.
|
Supermemory comes built with plugins for Claude Code, Cursor, Codex, 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" />
|
||||||
|
|
||||||
|
|
@ -129,18 +119,30 @@ These plugins are implementations of the supermemory API, and they are open sour
|
||||||
|
|
||||||
You can find them here:
|
You can find them here:
|
||||||
|
|
||||||
- Openclaw plugin: https://github.com/supermemoryai/openclaw-supermemory
|
- Claude Code plugin: https://github.com/supermemoryai/claude-supermemory
|
||||||
- Claude code plugin: https://github.com/supermemoryai/claude-supermemory
|
- Cursor plugin: https://github.com/supermemoryai/cursor-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 - Quick install
|
### MCP
|
||||||
|
|
||||||
```bash
|
Server URL:
|
||||||
npx -y install-mcp@latest https://mcp.supermemory.ai/mcp --client claude --oauth=yes
|
|
||||||
|
```text
|
||||||
|
https://mcp.supermemory.ai/mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `claude` with your client: `cursor`, `windsurf`, `vscode`, etc.
|
```json
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
|
||||||
|
|
@ -182,21 +184,6 @@ 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)
|
||||||
|
|
@ -271,7 +258,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.memories({
|
const results = await client.search({
|
||||||
q: "how do I deploy?",
|
q: "how do I deploy?",
|
||||||
containerTag: "user_123",
|
containerTag: "user_123",
|
||||||
searchMode: "hybrid",
|
searchMode: "hybrid",
|
||||||
|
|
@ -279,7 +266,7 @@ const results = await client.search.memories({
|
||||||
// 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.memories({
|
const results = await client.search({
|
||||||
q: "user preferences",
|
q: "user preferences",
|
||||||
containerTag: "user_123",
|
containerTag: "user_123",
|
||||||
searchMode: "memories",
|
searchMode: "memories",
|
||||||
|
|
@ -313,8 +300,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.memories()` | Hybrid search across memories and documents |
|
| `client.search()` | Hybrid search across memories and documents (`searchMode`) |
|
||||||
| `client.search.documents()` | Document search with metadata filters |
|
| `client.search.documents()` | Document search with metadata filters (legacy v3 response shape) |
|
||||||
| `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 |
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
直接用我们的应用,给自己搭一份专属的 supermemory。它会**在每次对话之间维护一张持久的记忆图谱**。
|
通过插件或 MCP 服务器,让 Claude Code、Cursor、Codex 和 OpenCode **在每次对话之间保持持久记忆**。
|
||||||
|
|
||||||
你的 AI 会记住你的偏好、项目、历史讨论——而且越用越聪明。
|
你的 AI 会记住你的偏好、项目、历史讨论——而且越用越聪明。
|
||||||
|
|
||||||
|
|
@ -85,38 +85,40 @@ Supermemory 是为 AI 设计的记忆与上下文层。在 **[LongMemEval](https
|
||||||
|
|
||||||
## 给你的 AI 装上记忆
|
## 给你的 AI 装上记忆
|
||||||
|
|
||||||
Supermemory 的应用、浏览器扩展、插件和 MCP 服务器,可以为任何兼容的 AI 助手提供持久记忆。装一次,AI 从此记住你。
|
插件和 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、OpenCode、OpenClaw、Hermes 提供了开箱即用的插件。
|
Supermemory 已经为 Claude Code、Cursor、Codex、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 agent(Supermemory 作为记忆 provider):https://github.com/NousResearch/hermes-agent
|
- Hermes agent(Supermemory 作为记忆 provider):https://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
|
||||||
```
|
```
|
||||||
|
|
||||||
把 `claude` 换成你用的客户端即可:`cursor`、`windsurf`、`vscode` 等等。
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"supermemory": {
|
||||||
|
"url": "https://mcp.supermemory.ai/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
更多 MCP 细节见:https://supermemory.ai/docs/supermemory-mcp/mcp
|
更多 MCP 细节见:https://supermemory.ai/docs/supermemory-mcp/mcp
|
||||||
|
|
||||||
|
|
@ -158,21 +160,6 @@ 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 构建
|
||||||
|
|
@ -247,7 +234,7 @@ const agent = new Agent(withSupermemory(config, "user-123", { mode: "full" }));
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 混合检索(默认)——一次查询同时跑 RAG 和记忆
|
// 混合检索(默认)——一次查询同时跑 RAG 和记忆
|
||||||
const results = await client.search.memories({
|
const results = await client.search({
|
||||||
q: "how do I deploy?",
|
q: "how do I deploy?",
|
||||||
containerTag: "user_123",
|
containerTag: "user_123",
|
||||||
searchMode: "hybrid",
|
searchMode: "hybrid",
|
||||||
|
|
@ -255,7 +242,7 @@ const results = await client.search.memories({
|
||||||
// 返回部署文档(RAG)+ 该用户的部署偏好(记忆)
|
// 返回部署文档(RAG)+ 该用户的部署偏好(记忆)
|
||||||
|
|
||||||
// 只查记忆
|
// 只查记忆
|
||||||
const results = await client.search.memories({
|
const results = await client.search({
|
||||||
q: "user preferences",
|
q: "user preferences",
|
||||||
containerTag: "user_123",
|
containerTag: "user_123",
|
||||||
searchMode: "memories",
|
searchMode: "memories",
|
||||||
|
|
@ -289,8 +276,8 @@ const { profile } = await client.profile({ containerTag: "user_123" });
|
||||||
|---|---|
|
|---|---|
|
||||||
| `client.add()` | 存储内容——文本、对话、URL、HTML |
|
| `client.add()` | 存储内容——文本、对话、URL、HTML |
|
||||||
| `client.profile()` | 一次调用返回用户画像 + 可选检索 |
|
| `client.profile()` | 一次调用返回用户画像 + 可选检索 |
|
||||||
| `client.search.memories()` | 跨记忆和文档的混合检索 |
|
| `client.search()` | 跨记忆和文档的混合检索(`searchMode`) |
|
||||||
| `client.search.documents()` | 带元数据过滤的文档检索 |
|
| `client.search.documents()` | 带元数据过滤的文档检索(旧版 v3 响应格式) |
|
||||||
| `client.documents.uploadFile()` | 上传 PDF、图片、视频、代码 |
|
| `client.documents.uploadFile()` | 上传 PDF、图片、视频、代码 |
|
||||||
| `client.documents.list()` | 列出和筛选文档 |
|
| `client.documents.list()` | 列出和筛选文档 |
|
||||||
| `client.settings.update()` | 配置记忆抽取与切分策略 |
|
| `client.settings.update()` | 配置记忆抽取与切分策略 |
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# PostHog Configuration
|
|
||||||
WXT_POSTHOG_API_KEY=your_posthog_project_api_key_here
|
|
||||||
26
apps/browser-extension/.gitignore
vendored
|
|
@ -1,26 +0,0 @@
|
||||||
# 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?
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
## supermemory Browser Extension
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,288 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -1,718 +0,0 @@
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,844 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,445 +0,0 @@
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,731 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,739 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
@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");
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
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>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
: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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
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>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 54 KiB |
|
|
@ -1,15 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7 KiB |
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./.wxt/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"types": ["chrome"]
|
|
||||||
},
|
|
||||||
"exclude": ["**/*.test.ts"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/**
|
|
||||||
* 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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
/**
|
|
||||||
* 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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
export function buildSearchMemoriesBody(
|
|
||||||
query: string,
|
|
||||||
containerTag?: string,
|
|
||||||
): {
|
|
||||||
q: string
|
|
||||||
include: { relatedMemories: boolean }
|
|
||||||
containerTag?: string
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
q: query,
|
|
||||||
include: { relatedMemories: true },
|
|
||||||
...(containerTag ? { containerTag } : {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,442 +0,0 @@
|
||||||
// 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 += `\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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
/**
|
|
||||||
* 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[]
|
|
||||||
}
|
|
||||||
|
|
@ -1,781 +0,0 @@
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
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"],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
---
|
|
||||||
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>
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
---
|
|
||||||
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
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
---
|
|
||||||
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
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
---
|
|
||||||
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"] }
|
|
||||||
```
|
|
||||||
248
apps/docs/agents-and-mcp.mdx
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
---
|
||||||
|
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>
|
||||||
|
|
@ -1,357 +0,0 @@
|
||||||
---
|
|
||||||
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>
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
---
|
|
||||||
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>
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
---
|
|
||||||
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>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: "NPM link"
|
|
||||||
url: "https://www.npmjs.com/package/@supermemory/tools"
|
|
||||||
icon: npm
|
|
||||||
---
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
---
|
|
||||||
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>
|
|
||||||
|
|
@ -1,357 +0,0 @@
|
||||||
---
|
|
||||||
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>
|
|
||||||
17
apps/docs/api-reference/connections.mdx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
18
apps/docs/api-reference/container-tags.mdx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
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)
|
||||||
21
apps/docs/api-reference/documents.mdx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
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)
|
||||||
21
apps/docs/api-reference/ingest.mdx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
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>
|
||||||
20
apps/docs/api-reference/memories.mdx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
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)
|
||||||
68
apps/docs/api-reference/overview.mdx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
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)
|
||||||
15
apps/docs/api-reference/profiles.mdx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
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)
|
||||||
19
apps/docs/api-reference/search.mdx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
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)
|
||||||
17
apps/docs/api-reference/settings.mdx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
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)
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
title: "Authentication"
|
title: "API keys & auth"
|
||||||
description: "API keys, scoped keys, and connector branding."
|
description: "Org API keys, container-scoped keys, and connector branding."
|
||||||
|
sidebarTitle: "API keys"
|
||||||
icon: "key"
|
icon: "key"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -55,13 +56,16 @@ This works for Google Drive, Notion, and OneDrive. See the full setup in [Custom
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Scoped API Keys
|
## Scoped API keys
|
||||||
|
|
||||||
<Accordion title="Container-scoped keys" icon="lock">
|
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.
|
||||||
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`
|
||||||
|
|
||||||
|
Scoped keys **cannot** read billing, manage org settings, or mint further keys.
|
||||||
|
|
||||||
### Create a scoped key
|
### Create a scoped key
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -79,7 +83,7 @@ This works for Google Drive, Notion, and OneDrive. See the full setup in [Custom
|
||||||
### 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 | — | 1–365 days |
|
| `expiresInDays` | No | — | 1–365 days |
|
||||||
|
|
@ -99,11 +103,11 @@ This works for Google Drive, Notion, and OneDrive. See the full setup in [Custom
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the returned key exactly like a normal API key — it just won't work outside its container scope.
|
Use the returned key like a normal API key — it just will not work outside its container scope.
|
||||||
|
|
||||||
### Disable a scoped key
|
### Disable a scoped key
|
||||||
|
|
||||||
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.
|
Revoke with the `id` from creation. Subsequent requests get `401`. Memories and container tags are **not** deleted.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl https://api.supermemory.ai/v3/auth/scoped-key/KEY_ID \
|
curl https://api.supermemory.ai/v3/auth/scoped-key/KEY_ID \
|
||||||
|
|
@ -111,9 +115,6 @@ This works for Google Drive, Notion, and OneDrive. See the full setup in [Custom
|
||||||
--header 'Authorization: Bearer YOUR_API_KEY'
|
--header 'Authorization: Bearer YOUR_API_KEY'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "success": true }
|
{ "success": true }
|
||||||
```
|
```
|
||||||
</Accordion>
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
---
|
|
||||||
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
|
|
||||||
|
|
@ -1,779 +0,0 @@
|
||||||
---
|
|
||||||
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>
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
---
|
|
||||||
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>
|
|
||||||
|
|
@ -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.memories({
|
const results = await client.search({
|
||||||
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.memories({ q: "planning", containerTag: "project_q1" });
|
await client.search({ 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,7 +170,10 @@ 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="Adding Memories" icon="plus" href="/add-memories">
|
<Card title="Scoped API keys" icon="key" href="/authentication#scoped-api-keys">
|
||||||
|
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>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
title: "Supported Content Types"
|
title: "Supported Content Types"
|
||||||
sidebarTitle: "Content Types"
|
sidebarTitle: "Multi-modal ingestion"
|
||||||
description: "All the content formats Supermemory can ingest and process"
|
description: "All the content formats Supermemory can ingest and process"
|
||||||
icon: "file-stack"
|
icon: "file-stack"
|
||||||
---
|
---
|
||||||
|
|
||||||
Supermemory automatically extracts and indexes content from various formats. Just send it—we handle the rest. See [Add Memories](/add-memories) to learn how to ingest content via the API.
|
Supermemory automatically extracts and indexes content from various formats. There are two entry points: `client.add()` for text and URLs, `client.documents.uploadFile()` for actual files. See [Add Memories](/ingestion/add-memories) to learn how to ingest content via the API.
|
||||||
|
|
||||||
## Text Content
|
## Text Content
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ Raw text, conversations, notes, or any string content.
|
||||||
```typescript
|
```typescript
|
||||||
await client.add({
|
await client.add({
|
||||||
content: "User prefers dark mode and uses vim keybindings",
|
content: "User prefers dark mode and uses vim keybindings",
|
||||||
containerTags: ["user_123"]
|
containerTag: "user_123"
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -29,11 +29,11 @@ Send a URL and Supermemory fetches, extracts, and indexes the content.
|
||||||
```typescript
|
```typescript
|
||||||
await client.add({
|
await client.add({
|
||||||
content: "https://docs.example.com/api-reference",
|
content: "https://docs.example.com/api-reference",
|
||||||
containerTags: ["documentation"]
|
containerTag: "documentation"
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Extracts:** Article text, headings, metadata. Strips navigation, ads, boilerplate.
|
**Extracts:** Article text, headings, metadata. Strips navigation, ads, boilerplate. URL extraction is powered by [Markdowner](https://md.dhr.wtf).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -41,11 +41,15 @@ await client.add({
|
||||||
|
|
||||||
### PDF
|
### PDF
|
||||||
|
|
||||||
|
Files are binary, so they go through `uploadFile`, not `add` — pass a stream, not base64:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await client.add({
|
import fs from 'fs';
|
||||||
content: pdfBase64,
|
|
||||||
contentType: "pdf",
|
await client.documents.uploadFile({
|
||||||
title: "Q4 Financial Report"
|
file: fs.createReadStream('report.pdf'),
|
||||||
|
containerTag: "user_123",
|
||||||
|
metadata: JSON.stringify({ title: "Q4 Financial Report" })
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -53,17 +57,13 @@ await client.add({
|
||||||
|
|
||||||
### Microsoft Office
|
### Microsoft Office
|
||||||
|
|
||||||
| Format | Extension | Content Type |
|
Word, Excel, and PowerPoint files upload the same way — Supermemory detects the type from the file itself:
|
||||||
|--------|-----------|--------------|
|
|
||||||
| Word | `.docx` | `docx` |
|
|
||||||
| Excel | `.xlsx` | `xlsx` |
|
|
||||||
| PowerPoint | `.pptx` | `pptx` |
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await client.add({
|
await client.documents.uploadFile({
|
||||||
content: docxBase64,
|
file: fs.createReadStream('roadmap.docx'),
|
||||||
contentType: "docx",
|
containerTag: "user_123",
|
||||||
title: "Product Roadmap"
|
metadata: JSON.stringify({ title: "Product Roadmap" })
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -78,18 +78,20 @@ Automatically handled via [Google Drive connector](/connectors/google-drive):
|
||||||
|
|
||||||
## Code & Markdown
|
## Code & Markdown
|
||||||
|
|
||||||
|
Both are plain text, so they go through `add` like any other string content — no file upload needed:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Markdown
|
// Markdown
|
||||||
await client.add({
|
await client.add({
|
||||||
content: markdownContent,
|
content: markdownContent,
|
||||||
contentType: "md",
|
containerTag: "user_123",
|
||||||
title: "README.md"
|
metadata: { title: "README.md" }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Code files (auto-detected language)
|
// Code (language auto-detected)
|
||||||
await client.add({
|
await client.add({
|
||||||
content: codeContent,
|
content: codeContent,
|
||||||
contentType: "code",
|
containerTag: "user_123",
|
||||||
metadata: { language: "typescript" }
|
metadata: { language: "typescript" }
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -102,11 +104,15 @@ Code is chunked using [code-chunk](https://github.com/supermemoryai/code-chunk),
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
|
|
||||||
|
`fileType: "image"` and `mimeType` are both required so Supermemory knows exactly how to process it:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await client.add({
|
await client.documents.uploadFile({
|
||||||
content: imageBase64,
|
file: fs.createReadStream('diagram.png'),
|
||||||
contentType: "image",
|
fileType: "image",
|
||||||
title: "Architecture Diagram"
|
mimeType: "image/png",
|
||||||
|
containerTag: "user_123",
|
||||||
|
metadata: JSON.stringify({ title: "Architecture Diagram" })
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -118,19 +124,24 @@ await client.add({
|
||||||
|
|
||||||
## Audio & Video
|
## Audio & Video
|
||||||
|
|
||||||
|
Video has a dedicated `fileType`; audio is uploaded the same way and detected from the file itself:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Audio
|
// Video
|
||||||
await client.add({
|
await client.documents.uploadFile({
|
||||||
content: audioBase64,
|
file: fs.createReadStream('demo.mp4'),
|
||||||
contentType: "audio",
|
fileType: "video",
|
||||||
title: "Customer Call Recording"
|
mimeType: "video/mp4",
|
||||||
|
containerTag: "user_123",
|
||||||
|
metadata: JSON.stringify({ title: "Product Demo" })
|
||||||
});
|
});
|
||||||
|
|
||||||
// Video
|
// Audio
|
||||||
await client.add({
|
await client.documents.uploadFile({
|
||||||
content: videoBase64,
|
file: fs.createReadStream('call-recording.mp3'),
|
||||||
contentType: "video",
|
mimeType: "audio/mpeg",
|
||||||
title: "Product Demo"
|
containerTag: "user_123",
|
||||||
|
metadata: JSON.stringify({ title: "Customer Call Recording" })
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -142,13 +153,15 @@ await client.add({
|
||||||
|
|
||||||
## Structured Data
|
## Structured Data
|
||||||
|
|
||||||
|
JSON and CSV are text — stringify and send them through `add()`, no file upload needed.
|
||||||
|
|
||||||
### JSON
|
### JSON
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await client.add({
|
await client.add({
|
||||||
content: JSON.stringify(userData),
|
content: JSON.stringify(userData),
|
||||||
contentType: "json",
|
containerTag: "user_123",
|
||||||
title: "User Profile Data"
|
metadata: { title: "User Profile Data", format: "json" }
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -157,8 +170,8 @@ await client.add({
|
||||||
```typescript
|
```typescript
|
||||||
await client.add({
|
await client.add({
|
||||||
content: csvContent,
|
content: csvContent,
|
||||||
contentType: "csv",
|
containerTag: "user_123",
|
||||||
title: "Sales Data Q4"
|
metadata: { title: "Sales Data Q4", format: "csv" }
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -166,26 +179,33 @@ await client.add({
|
||||||
|
|
||||||
## File Upload
|
## File Upload
|
||||||
|
|
||||||
For binary files, encode as base64:
|
For any binary file, use `uploadFile` — it accepts a stream, not base64:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { readFileSync } from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
const file = readFileSync('./document.pdf');
|
await client.documents.uploadFile({
|
||||||
const base64 = file.toString('base64');
|
file: fs.createReadStream('./document.pdf'),
|
||||||
|
containerTag: "user_123",
|
||||||
await client.add({
|
metadata: JSON.stringify({ title: "document.pdf" })
|
||||||
content: base64,
|
|
||||||
contentType: "pdf",
|
|
||||||
title: "document.pdf"
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
No Node `fs` access? `uploadFile` also accepts a web `File`, a `fetch` `Response`, or the SDK's `toFile` helper:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Supermemory, { toFile } from 'supermemory';
|
||||||
|
|
||||||
|
await client.documents.uploadFile({ file: new File(['my bytes'], 'file') });
|
||||||
|
await client.documents.uploadFile({ file: await fetch('https://somesite/file') });
|
||||||
|
await client.documents.uploadFile({ file: await toFile(Buffer.from('my bytes'), 'file') });
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Auto-Detection
|
## Auto-Detection
|
||||||
|
|
||||||
If you don't specify `contentType`, Supermemory auto-detects:
|
`add()` tells URLs and plain text apart on its own — no extra flag needed:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// URL detected automatically
|
// URL detected automatically
|
||||||
|
|
@ -195,9 +215,7 @@ await client.add({ content: "https://example.com/page" });
|
||||||
await client.add({ content: "User said they prefer email contact" });
|
await client.add({ content: "User said they prefer email contact" });
|
||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
For files, `uploadFile` detects type from the file itself in most cases. `fileType` only exists to force specific processing — and it's required (along with `mimeType`) for images and video.
|
||||||
For binary content (files), always specify `contentType` for reliable processing.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -209,6 +227,8 @@ For binary content (files), always specify `contentType` for reliable processing
|
||||||
| Files | 50MB |
|
| Files | 50MB |
|
||||||
| URLs | Fetched content up to 10MB |
|
| URLs | Fetched content up to 10MB |
|
||||||
|
|
||||||
|
**Typical processing time:** text is near-instant; PDFs take 1-5s; images 2-10s; video 10s+; webpages 1-3s. Text content is chunked at the sentence level with a 2-sentence overlap between chunks.
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
For large files, consider chunking or using [connectors](/connectors/overview) for automatic sync.
|
For large files, consider chunking or using [connectors](/connectors/overview) for automatic sync.
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
@ -218,7 +238,7 @@ For large files, consider chunking or using [connectors](/connectors/overview) f
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
<CardGroup cols={2}>
|
||||||
<Card title="Add Memories" icon="plus" href="/add-memories">
|
<Card title="Add Memories" icon="plus" href="/ingestion/add-memories">
|
||||||
Upload content via the API
|
Upload content via the API
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Super RAG" icon="bolt" href="/concepts/super-rag">
|
<Card title="Super RAG" icon="bolt" href="/concepts/super-rag">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Customizing for Your Use Case"
|
title: "Customizing for Your Use Case"
|
||||||
sidebarTitle: "Customization"
|
sidebarTitle: "Customizing"
|
||||||
description: "Configure Supermemory's behavior for your specific application"
|
description: "Configure Supermemory's behavior for your specific application"
|
||||||
icon: "settings-2"
|
icon: "settings-2"
|
||||||
---
|
---
|
||||||
|
|
@ -70,6 +70,16 @@ await client.settings.update({
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
|
|
||||||
|
### Related settings
|
||||||
|
|
||||||
|
`shouldLLMFilter` must be `true` for any of these to take effect — using them without it returns a 400 error.
|
||||||
|
|
||||||
|
| Setting | Type | Limits |
|
||||||
|
|---------|------|--------|
|
||||||
|
| `categories` | `string[]` | 1-50 chars each. If omitted, 3-5 categories are auto-generated |
|
||||||
|
| `includeItems` / `excludeItems` | `string[]` | 1-20 chars each item |
|
||||||
|
| `filterPrompt` | `string` | 1-750 characters |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Entity Context
|
## Entity Context
|
||||||
|
|
@ -193,7 +203,7 @@ Settings are organization-wide. Changes apply to new content only—existing mem
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
<CardGroup cols={2}>
|
||||||
<Card title="Add Memories" icon="plus" href="/add-memories">
|
<Card title="Add Memories" icon="plus" href="/ingestion/add-memories">
|
||||||
See your custom settings in action
|
See your custom settings in action
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Connectors" icon="plug" href="/connectors/overview">
|
<Card title="Connectors" icon="plug" href="/connectors/overview">
|
||||||
|
|
|
||||||