Compare commits

..

119 commits

Author SHA1 Message Date
MaheshtheDev
5258cb74c8 chore(web): reduce the app to a redirect shell, drop the browser extension (#1651)
chore(web): reduce the app to a redirect shell, drop the browser extension

app.supermemory.ai now forwards everything to the console: plugin, OAuth and invite paths get an immediate 308 with the query intact, everything else shows a short notice first. Removes the browser extension workspace.

chore(web): give the moved notice a proper design

Hostnames become the headline, one primary action, a draining line for the countdown, DM Sans and the dot-grid backdrop from the brand.

chore(docs): point docs and README at the console, drop stale sections

Docs and both READMEs now link to console.supermemory.ai for API keys. Removed the company-brain docs tab with a redirect, and trimmed the README app section.

chore(web): give the redirect five seconds
2026-09-07 19:56:46 +00:00
MaheshtheDev
4d8a4ebfdd fix(mcp): let getDocument read any accessible document (#1641)
Some checks failed
Publish Pipecat SDK Python / publish (push) Has been cancelled
Publish Agent Framework Python / publish (push) Has been cancelled
Publish AI SDK / publish (push) Has been cancelled
Publish Cartesia SDK Python / publish (push) Has been cancelled
Publish OpenAI SDK Python / publish (push) Has been cancelled
Publish Tools / publish (push) Has been cancelled
getDocument filtered on the caller's active space, so an ID from listDocuments in any other space returned "Document not found". With activeSpace unset the fallback is sm_project_default, which broke most cross-space reads.

The API already scopes document reads to the caller's org, so the extra filter added no protection. Verified locally against the mono API: own-space and cross-space IDs now resolve, foreign-org IDs still 404.
2026-09-02 21:49:57 +00:00
Luv
9a0c5a5ad6
docs: fixed typo in graph-memory.mdx (#1640) 2026-09-02 10:07:03 -07:00
MaheshtheDev
17eab43cd4 docs: add Cursor and Grok Bot plugin pages (#1635)
Ship dedicated integration docs and point the plugin catalog at them. Grok Bot stays to install, auth, and skills — no Cursor-only config or repo tags.
2026-09-01 20:51:54 +00:00
Dhravya
4ad5f0beb1
feat(sdk-playground): reflect SDK-owned memory block in debug view (#1533)
## Stack Context

Part 3 (top) of a 3-PR stack moving memory deduplication into the SDKs. See `sdk-dedup/tools-ts` for full context.

## What?

Update the SDK playground so its debug view reflects the SDK-owned memory block.

- Displays the current deduplicated `<supermemory>` replacement block produced by the SDK middleware, instead of the old browser-side "seen facts" delta.
- Adds a `memory-dedupe` helper and ignores local `*.tsbuildinfo`.

## Why?

The previous debug cards were misleading — they showed an incremental browser-filtered delta while the middleware actually re-injected the full profile. Now the visualization matches what the SDK really sends.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Playground-only visualization and chat gating changes; no production SDK or API behavior.
>
> **Overview**
> The playground **debug trace** now shows the **deduplicated memory block** the SDK middleware would inject (static → dynamic → search, mode-aware), instead of a misleading browser-side “new facts” delta. A new **`memory-dedupe`** helper mirrors `@supermemory/tools` middleware behavior and is applied when fetching container context and building middleware memory debug entries; the context preview card is relabeled to reflect that each turn **replaces** the prior `<supermemory>` block.
>
> **Chat UX:** messaging is enabled when API keys are configured on the **server** (`hasSupermemoryKey` / `hasOpenAiKey` from `/api/chat`), not only when keys are typed in the panel. The message input stays editable while waiting for text; Send still requires non-empty input.
>
> Also ignores `*.tsbuildinfo` in `.gitignore`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ed15364eb3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-09-01 06:10:37 +00:00
Dhravya
03773c4f2e
feat(python-sdks): SDK-level cross-source memory deduplication (#1532)
## Stack Context

Part 2 of a 3-PR stack moving memory deduplication into the SDKs. See `sdk-dedup/tools-ts` (parent) for the full context and the TypeScript implementation this mirrors.

## What?

Port the normalized, priority-ordered (`static > dynamic > search`) profile deduplication into the Python SDKs.

- Each request injects one **owned memory block that replaces** the prior block rather than accumulating.
- Dedup is **request-local** (no shared state), so it stays correct under concurrency.

Covers OpenAI, Agent Framework (middleware + context provider), Cartesia, and Pipecat.

## Why?

Keeps the Python SDKs at behavioral parity with the TypeScript SDK so all integrations deduplicate memory the same way.

## Testing

- OpenAI: 31 passed, 11 skipped (live)
- Agent Framework: 59 passed
- Cartesia: 8 passed
- Pipecat: 8 passed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes memory formatting and system-prompt injection across multiple SDK integrations; incorrect dedup or replacement could alter LLM context, but there is no auth or data-store risk.
>
> **Overview**
> Ports **normalized cross-source memory deduplication** and **replace-not-append injection** into the Python OpenAI, Agent Framework, Cartesia, and Pipecat packages so they match the TypeScript SDK behavior.
>
> **Deduplication** uses request-local keys: strip optional `[YYYY-MM-DD]` prefixes, normalize whitespace, and compare with `casefold`, with priority **static → dynamic → search**. In **`query` mode**, profile static/dynamic are excluded from dedup input so facts that only appear in search (or overlap profile) are not dropped before formatting.
>
> **Injection** no longer appends memory text every turn. OpenAI and Agent Framework middleware **strip prior owned `<supermemory context="user-memories" readonly>` blocks** and **replace** them once per request while keeping the caller’s system instructions; extra system messages lose stale blocks only. New helpers (`strip`/`replace`/`wrap`) live in each package’s utils.
>
> Tests cover normalized fact variants, query-mode search retention, and stale block replacement.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 42f308b224. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-09-01 06:10:36 +00:00
Dhravya
d0f53b0d64
feat(tools): SDK-level cross-source memory deduplication (#1531)
## Stack Context

This stack moves memory deduplication **out of the playground UI and into the SDKs themselves**, so every integration injects a single, deduplicated, self-replacing memory block. Three PRs:

1. **`sdk-dedup/tools-ts`** (this PR) — TypeScript SDK core + integrations
2. `sdk-dedup/python` — Python SDKs
3. `sdk-dedup/playground` — playground debug view reflects the SDK-owned block

## What?

Move profile deduplication into the SDK middleware for the TypeScript tools package.

- Facts are normalized (strip leading `[YYYY-MM-DD]`, trim, collapse whitespace, casefold) and deduplicated in **`static > dynamic > search`** priority within a single request.
- The result is injected as one **owned `<supermemory>` block** that *replaces* the previous block instead of accumulating a new one each turn.
- Dedup is **mode-aware**: in query mode, search results are not dropped against a profile that isn't being injected.
- Deduplication is **request-local** — no global/browser `Set`. Safe for multiple users, concurrent requests, and Cloudflare Worker isolates.

Covers AI SDK, OpenAI (Chat + Responses), Mastra, and VoltAgent. New `shared/memory-context.ts` owns the block-replacement logic.

## Why?

The earlier "conversation-scoped deduplication" was only a playground browser `Set` — a UI debug affordance that did not change what the SDK sent to the model, and would have been unsafe as server-side global state. Real cross-source dedup belongs in the SDK, applied fresh per stateless model request.

## Testing

- `bun run test` in `packages/tools`: 145 passed (the one failing suite, `claude-memory.test.ts`, is a pre-existing broken import unrelated to this change).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes how system prompts and instructions are built across all TypeScript integrations; behavior is well-covered by unit tests but incorrect strip/replace logic could drop or duplicate context in production prompts.
>
> **Overview**
> Moves **cross-source memory deduplication** and **owned prompt injection** into `@supermemory/tools` so every integration sends one deduplicated memory block per request instead of growing context each turn.
>
> **Deduplication:** Facts are normalized via `normalizeMemoryFact` (strip `[YYYY-MM-DD]`, trim, collapse whitespace, lowercase) and deduplicated with **static → dynamic → search** priority. `deduplicateMemoriesForMode` keeps search hits in **query** mode when the profile is not injected.
>
> **Owned `<supermemory>` block:** New `shared/memory-context.ts` wraps memories in `<supermemory context="user-memories" readonly>`, strips stale blocks, and **replaces** prior SDK context while preserving caller system instructions. Applied in AI SDK (`injectMemoriesIntoParams`), OpenAI Chat/Responses middleware, Mastra input processor (`wrapMemoryContext`), and VoltAgent hooks.
>
> **Tests:** Unit coverage for block replacement (with-supermemory, OpenAI, VoltAgent), Mastra wrapper tag assertion, normalized dedup variants, and concurrent `containerTag` isolation.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2fa2e0d85c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-09-01 06:10:36 +00:00
Dhravya
b01d2b69a3
feat(sdk-playground): interactive SDK chat playground (#1437)
## Summary
- Add `apps/sdk-playground` — chat UI to test TS/Python SDK integrations
- Context panel with document memories, API keys in dashboard, tools reference tab
- Python FastAPI server on port 8792; portless entry in `portless.json`

Stacked on #1436

## Test plan
- [ ] `cd apps/sdk-playground && bun run check-types`
- [ ] `bun run dev` with Supermemory + OpenAI keys in UI
- [ ] Switch SDKs and verify chat + context panel

Made with [Cursor](https://cursor.com)
2026-09-01 06:10:36 +00:00
Dhravya
7974498062
chore(ci): wire JS tools unit tests into CI (#1436)
## Summary
- Add tools/ai-sdk unit test jobs to `ci.yml`
- Update `turbo.json` and root `package.json` scripts
- Refresh `bun.lock`

Stacked on #1435

## Test plan
- [ ] CI passes on this branch

Made with [Cursor](https://cursor.com)
2026-09-01 06:10:36 +00:00
Dhravya
4173edb9e6
docs(skills): refresh Supermemory skill for 7-tool parity (#1435)
## Summary
- Update `SKILL.md` with proactive search and full 7-tool surface
- Refresh `sdk-guide.md` with v4 API examples and tool descriptions

Stacked on #1434

## Test plan
- [ ] Review skill content for accuracy

Made with [Cursor](https://cursor.com)
2026-09-01 06:10:36 +00:00
Dhravya
c262cc9953
fix(python-sdks): v4 API migration for integration packages (#1434)
## Summary
- **agent-framework**: proactive search tool descriptions
- **cartesia / pipecat**: v4 `client.add` + hybrid search, dedupe fixes, tests

Stacked on #1433

## Test plan
- [ ] pytest in agent-framework, cartesia, pipecat packages

Made with [Cursor](https://cursor.com)
2026-09-01 06:10:36 +00:00
Dhravya Shah
879ddd5c95
docs: clarify customer content is never used to train models on any plan (#1613)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-31 23:09:30 -07:00
Dhravya
46d1b53230
feat(ai-sdk): re-export 7-tool surface (#1433)
## Summary
- Re-export full tool set from `@supermemory/tools/ai-sdk`
- Add unit tests for tool re-exports

Stacked on #1432

## Test plan
- [ ] `bun run test:unit` in `packages/ai-sdk`

Made with [Cursor](https://cursor.com)
2026-09-01 05:59:58 +00:00
Dhravya
de3bbb3ce9
feat(tools): 7-tool parity and description refresh (#1432)
## Summary
- Refresh canonical tool descriptions in `tools-shared.ts`
- Align OpenAI and AI SDK tool bindings with 7-tool surface
- Export `TOOL_DESCRIPTIONS` / `PARAMETER_DESCRIPTIONS` from package index

Stacked on #1431

## Test plan
- [ ] `bun run test:unit` in `packages/tools`

Made with [Cursor](https://cursor.com)
2026-09-01 05:59:57 +00:00
Dhravya Shah
5fee2f2872
docs(mcp): fix grammar in ChatGPT web plugin description (#1630)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-31 21:46:32 -07:00
Dhravya Shah
ece20ff53e
chore(ci): Python SDK pytest workflow (#1431)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 21:39:36 -07:00
Dhravya
348483d5e8
feat(openai-sdk-python): 7-tool parity (#1430)
## Summary
- Expand `SupermemoryTools` from 2 tools to 7 (matches `@supermemory/tools`)
- Add `memory_forget` via shared HTTP helper
- Add document list/add/delete and get_profile tool surfaces
- Expand tests for new tools and execution paths

Stacked on #1429

## Test plan
- [x] `uv run pytest tests/test_tools.py::TestMemoryOperationsUnit`

Made with [Cursor](https://cursor.com)
2026-09-01 04:34:18 +00:00
Dhravya
c5b7e7d4fc
fix(openai-sdk-python): migrate to v4 Supermemory APIs (#1429)
## Summary
- Replace deprecated `search.execute` with `search.memories` (hybrid mode) in `search_memories`
- Replace `memories.add` with `client.add` in tools and middleware
- Fix middleware `container_tag` param (was incorrectly `container_tags`)
- Fix profile memory deduplication for string and Pydantic API items
- Bump `supermemory>=3.50` and `requires-python>=3.9`

## Test plan
- [x] `uv run pytest tests/test_tools.py::TestMemoryOperationsUnit`

Made with [Cursor](https://cursor.com)
2026-09-01 04:34:18 +00:00
MaheshtheDev
143024fa34 chore(web): forward legacy auth paths (#1631)
Forwards /auth/connect and /auth/agent-connect with the query string intact, and drops the pages they replaced.
2026-09-01 00:55:16 +00:00
MaheshtheDev
9ccd1b64c3 chore(web): clean up onboarding and dashboard surfaces (#1614)
Removes stale promo cards, banners, and the setup modal. Simplifies the onboarding entry so all new workspaces go through one flow.
2026-08-29 04:57:09 +00:00
Aditya kumar singh
d436792e77
docs: document self-hosted v0.0.5 model mixing bug and v0.0.7 resolution (#1450) (#1606) 2026-08-27 16:38:11 -07:00
MaheshtheDev
29c43984fe feat(web): pause Google Drive connect with a notify-me fallback (#1602)
![image.png](https://app.graphite.com/user-attachments/assets/6d7dd2cf-575b-450c-b7bb-c24b8ec834b9.png)

Google is not approving new authorizations while it re-reviews our app, so
every path that starts a new Google connection now shows a PAUSED badge and a
"Notify me" button instead of Connect.

- Existing connections are untouched: sync, file picking and history still work.
- Notify me fires a connector_paused_clicked PostHog event and remembers the
choice in localStorage, so we can pull who to email when the review clears.
- One list in lib/connector-availability.ts drives every surface; removing the
two entries turns Google back on.
2026-08-27 20:20:13 +00:00
MaheshtheDev
f11d8c4620 feat(web): drop the Web research row from Company Brain models (#1600)
The `research` / `researchEffort` model role no longer exists on the API,
so remove its card, its preset entries, and its types.
2026-08-26 19:07:56 +00:00
MaheshtheDev
9652478093 feat(web): offer a setup call alongside Slack install (#1599)
- First-landing modal on the Company Brain home: book a 30-min setup call, or install Slack yourself. Dismissal stored in localStorage.
- Setup call also reachable from a header icon, the user menu, and the onboarding research rail.
- Drops the desktop Invite button from the header; invite stays in the stats row and mobile menu.
2026-08-26 07:43:48 +00:00
MaheshtheDev
3f7b9667c6 chore: remove two dead onboarding routes and a note-content console.log (#1596)
Cherry-picks two cleanup PRs and finishes the job. Net 262 deletions.

- #1473 (@abhay-codes07): drops a `console.log` in the fullscreen note editor that printed the whole note body on every keystroke, which PostHog session replay can capture.
- #1563 (@ishaanxgupta): removes `/api/onboarding/research` and `/api/onboarding/extract-content`. Neither has a caller anywhere in the repo, and both spent metered Exa and xAI quota. This reverts the guards added for them in #1589, which only existed to make unreachable code safe.
- On top: `EXA_API_KEY`, `XAI_API_KEY` and the `@ai-sdk/xai` dependency are removed, since deleting those routes left them with no consumer.

Co-Authored-By: abhay-codes07 <182421137+abhay-codes07@users.noreply.github.com>
Co-Authored-By: ishaanxgupta <124028055+ishaanxgupta@users.noreply.github.com>
2026-08-25 10:19:47 +00:00
MaheshtheDev
e4afc770be feat(tools): apiKey option, type re-exports, and two reliability fixes (#1594)
Some checks failed
Publish Tools / publish (push) Has been cancelled
Cherry-picks four contributor PRs for `@supermemory/tools` onto one branch, and bumps the package to 2.2.0.

- #1244 (@rajarshidattapy): `withSupermemory` accepts `options.apiKey` instead of only reading `SUPERMEMORY_API_KEY`, matching the Vercel, Mastra and Voltagent integrations. Unblocks secrets managers, edge runtimes and per-request keys.
- #1574 (@Agnik47): re-exports `PromptTemplate`, `MemoryPromptData` and `WithSupermemoryOptions` from `ai-sdk`. `./vercel` is not a published subpath, so the documented custom-template example did not compile.
- #1488 (@abhinav7x94): malformed tool-call JSON returns an error result instead of throwing out of the request.
- #1507 (@abhinav7x94): VoltAgent `onEnd` awaits the conversation save, which was fire-and-forget and could be dropped when a serverless runtime tore down.

Dropped the `middleware.test.ts` added by #1244. Note that editing `packages/tools/package.json` triggers the npm publish workflow on merge.

Co-Authored-By: rajarshidattapy <138959719+rajarshidattapy@users.noreply.github.com>
Co-Authored-By: Agnik47 <140933190+Agnik47@users.noreply.github.com>
Co-Authored-By: abhinav7x94 <204053250+abhinav7x94@users.noreply.github.com>
2026-08-24 22:39:55 +00:00
MaheshtheDev
f051af098e fix(mcp): bound tool inputs and scope get_document to the active space (#1593)
Cherry-picks #1582, #1583, #1584 and #1585 from @Sravanjangam (security audit #1578) onto one branch.

- MCP: `get_document` scopes to the active space like its sibling read tools, `fetch-graph-data` bounds page/limit, `guided-save` caps prefill at 200k, and `whoAmI` no longer returns the transport session id.
- ai-sdk: search limit clamped to 1-50 with a 30s client timeout.
- validation: caps on `DocumentsWithMemoriesQuerySchema.limit` and `BulkDeleteMemoriesSchema.containerTags`.
- Raycast: `metadata.url` is parsed and only http(s) is offered to the OS opener.

Dropped his `add_memory` permission gate: it checked the target against the list of existing spaces, so writes to a new space failed and the no-active-space path surfaced `No write access to space "undefined"`. Write permission stays enforced in the API via `containerTagGate`.

Hardening and consistency rather than a security fix, since the API already enforces every permission boundary here.

Co-Authored-By: Sravanjangam <163002695+Sravanjangam@users.noreply.github.com>
2026-08-24 21:36:04 +00:00
MaheshtheDev
6cae175852 fix(mcp): return 503 on auth-backend outages instead of invalid_token (#1591)
Cherry-picks #1587 from @Sravanjangam, plus the OAuth half on top.

When the auth backend is slow or returns a 5xx, the MCP server currently answers `invalid_token`. That is the protocol's signal to discard the credential and re-authenticate, so a brief upstream blip logs every connected client out, and `sm_` API key users have no automatic way back. These requests now return 503 with `Retry-After: 5` so clients retry instead.

His change covered the API key path only. This shares one `transientAuthErrorFor` helper between `validateApiKey` and `validateOAuthToken`, so a JWKS timeout or a 5xx also returns 503 on the OAuth path that Claude, Cursor and browser clients use.

Genuinely bad tokens are unaffected: bad signature, expired, and no-matching-key still resolve to 401. Verified across all seven cases.

Co-Authored-By: Sravanjangam <163002695+Sravanjangam@users.noreply.github.com>
2026-08-24 15:53:06 +00:00
MaheshtheDev
3b0fc9c959 fix(web): authenticate and bound metered /api routes (#1589)
Cherry-picks #1579 and #1580 from @Sravanjangam (security audit #1578), plus improvements on top.

- `/api/og`, `/api/onboarding/extract-content` and `/api/onboarding/research` now verify the session against the auth backend; the middleware only checked that a cookie was present, so a forged cookie reached handlers that spend metered Exa/xAI quota.
- Bounds those routes: 2MB cap on fetched HTML, max 10 http(s) URLs per request, name/email length limits and a 60s timeout on the LLM call.
- De-duplicates URLs before calling Exa, and collapses whitespace in `name`/`email` so a newline can't forge extra prompt lines. Both adapted from @SEPURI-SAI-KRISHNA's #1528 and #1530.
- Deletes the unused, unauthenticated `account-status` route.

Verified locally: pre-fix `/api/og` returned 200 for a forged cookie, post-fix it returns 401. Five duplicate URLs collapse to two before reaching Exa, and a newline-laden `name` arrives as a single prompt line.

Supersedes #1528 and #1530.
2026-08-23 21:48:46 +00:00
Dhravya
3487666481
feat(web): add MCP connector directory (#1461)
<!-- VORFLUX_AGENT_PR_BODY_BEGIN -->
Adds the full 654-entry MCP directory without bundling records into client JavaScript, with explicit capability status and connector branding that degrades safely when no authoritative logo is available.

## Changes

- Lazy-load and validate the searchable, filterable, progressively rendered MCP catalog.
- Render same-origin proxied provider icons for 543 entries, with a reviewed domain allowlist and deterministic fallback marks for 111 unresolved or unbranded entries.
- Record OAuth discovery capability separately from end-to-end support; all directory setup actions remain suppressed until their authentication flow is verified.
- Add a reproducible OAuth metadata probe with HTTPS/private-network protections, stable URL keys, authorization-server scanning, and catalog fingerprint validation.
- Add Google Drive branding for the curated built-in connector.

## Testing

- **Passed:** Deterministic generation and catalog assertions.
  ```bash
  PATH="$HOME/.bun/bin:$PATH" python3 apps/web/scripts/generate-mcp-directory.py --output
  cmp apps/web/public/mcp-directory.json
  ```
  Verified 654 entries, 254 DCR discoveries, 27 preregistered OAuth discoveries, 373 unclassified entries, and zero directory setup actions.
- **Passed:** Stale OAuth metadata fingerprint is rejected by the generator.
- **Passed:** Touched-file Biome checks and `git diff --check`.
- **Passed:** Icon proxy returned 200 for an allowlisted domain and 400 for an unknown valid-looking domain.
- **Passed:** Authenticated desktop/mobile browser inspection and conservative capability labels.
- **Passed:** Public preview returned HTTP 200 and rendered the real app. Authentication cookies do not transfer to the public hostname, so the public screenshot shows login.
- **Partial:** Repository-wide TypeScript checks remain blocked by unrelated existing errors outside the touched MCP files.
- **Partial:** 111 entries intentionally retain deterministic fallback marks; endpoint-derived domains may not always be the canonical brand logo.
- **Blocked:** Google rejected the local HTTP OAuth callback, so live Google Drive consent, callback, persistence, tool discovery, disconnect, and reconnect were not completed.

Public preview: https://ar8ruchhbi65.preview.us1.vorflux.com/configure/tools

---
**Attached Images**

*[288.csv]*

*[mcp-directory-final.json]*

![mcp-directory-branding-desktop.png](https://api.us1.vorflux.com/assets/artifacts/c3VwZXJtZW1vcnk6Zjo4MDA0.3_UzR_OP9Jk228FYbrAPTXyqybRBlqwn5Uv4tksf_Y0.png)

![mcp-directory-branding-mobile.png](https://api.us1.vorflux.com/assets/artifacts/c3VwZXJtZW1vcnk6Zjo4MDA1.b5G6nsOBVm2s6DlEFWFiMFCcULAkV0MCCGZ8XVsA5js.png)

![mcp-directory-public-preview.png](https://api.us1.vorflux.com/assets/artifacts/c3VwZXJtZW1vcnk6Zjo4MDA2.ZrBAeBi62JX1xavAtaDLQ0fuixgBjN7x1NrqIxtdmKw.png)
<!-- VORFLUX_AGENT_PR_BODY_END -->

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/1cd0aab9-2a45-4818-aa13-f9bfe032ddba)
- Requested by: Dhravya Shah (dhravya@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes how users pick MCP URLs and auth (OAuth vs API key) before hitting existing connect endpoints; no new backend auth logic in this diff, but misconfiguration or trusting bad URLs remains a user-risk surface.
>
> **Overview**
> Adds a **browseable MCP directory** on the Company Brain connectors page: the catalog is **not bundled in JS**—it loads from static **`/mcp-directory.json`** only after the user opens the directory (with validation, caching, and abort handling).
>
> The new **`McpDirectoryBrowser`** supports search, category/availability filters, and progressive “show more” rendering. Supported remote entries route into the existing custom MCP flow via **Set up**, which pre-fills name/URL and opens the connector dialog with context-specific copy.
>
> The custom connector dialog now uses an explicit **OAuth vs API key** toggle; API key fields only appear for API-key mode, and directory-backed connections get **stable slugs** (`-dir-` suffix) so names display cleanly on connected cards. **Middleware** excludes `mcp-directory.json` from the auth matcher so the asset can be fetched publicly.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8b59bae84a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-08-20 22:58:39 +00:00
ishaanxgupta
dda56e766e Add plugin CLI command guide to integrations (#1534)
## Summary

- add an “Install plugins with one command” action beside the Plugins section

<img width="1280" height="651" alt="image" src="https://github.com/user-attachments/assets/a2e19a91-c94c-4a56-87c4-b72c4dbe89ba" />
<img width="1280" height="554" alt="image" src="https://github.com/user-attachments/assets/2cd4b3a4-ac30-4411-8221-baca09e90b0f" />
2026-08-20 18:07:31 +00:00
Dhravya Shah
818a83a381
fix(mcp): strip API extras from listMemories entries (#1539)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 14:05:52 -07:00
Aditya Mishra
7b1175cb1a
fix(browser-extension): run wxt prepare before type checking (#1557) 2026-08-19 19:23:52 +05:30
Rajarshi Datta
20410a6862
fix(ui): remove the unused, broken AnonymousAuth component (#1555) 2026-08-19 19:22:17 +05:30
Rajarshi Datta
7d59070ad6
fix(web): scope the ?view=mcp guest exemption to / (#1553) 2026-08-19 18:33:58 +05:30
Dhravya
18a2dfbe39
feat(mcp): accept Supermemory API keys as Bearer auth (#1537)
## Stack Context

Single-auth story for the Claude Code supermemory plugin rework: the plugin's hooks and its MCP surface share one credential (`sm_` API key from the existing browser connect flow). That requires `mcp.supermemory.ai` to accept plain API keys, which it currently rejects (OAuth JWT only).

## What?

- `validateApiKey()` in `server/auth`: `sm_`-prefixed Bearer tokens validate via the existing `fetchSession()` (`GET /v3/session`) and map to the same `AuthUser` shape as OAuth tokens (`userId` ← `user.id`, `organizationId` ← `org.id`, the key itself as `bearerToken` for downstream API calls). Successful lookups cached per isolate for 60s.
- `handleMcpRequest` routes by token shape: `sm_` keys → session validation, everything else → OAuth JWT verification (unchanged).
- `sessionInfoSchema` now types the `org.id` field the session endpoint already returns.

## Why?

MCP clients that already hold an API key (Claude Code plugin hooks, CLI, scripts) can connect without an OAuth dance or a second consent. OAuth behavior is untouched — the existing "rejects opaque API keys" test on the OAuth validator still passes; keys just get their own path. Malformed keys are rejected without an API round-trip.

Tests: 4 new cases (valid key → AuthUser, cache hit → single fetch, 401 → null, malformed → no request). `vitest run src/server/auth` 13/13, `tsc --noEmit` clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new authentication path on the MCP entrypoint with in-memory key caching (60s TTL), so revoked keys may remain valid briefly within an isolate; OAuth behavior is unchanged.
>
> **Overview**
> MCP Bearer auth now accepts **`sm_` Supermemory API keys** in addition to OAuth JWTs, so clients that already hold an API key can connect without OAuth.
>
> **`validateApiKey`** treats keys matching `sm_` plus at least 17 non-space characters as API keys: it calls **`GET /v3/session`** with the key as Bearer, maps **`user.id`** and **`org.id`** into the same **`AuthUser`** shape as OAuth (key kept as **`bearerToken`** for downstream API calls), and caches successful results per isolate for **60s** (up to 1000 entries, full clear on overflow). Malformed keys are rejected locally with no HTTP call; session **401** yields unauthenticated.
>
> **`handleMcpRequest`** branches on token shape: API keys go through session validation; other tokens still use JWT verification unchanged.
>
> **`sessionInfoSchema`** now includes optional **`org.id`** typing for session responses used when resolving organization context from API keys.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e54fb11bf1598d07a807eb2b0b63a347aaa58fb6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-08-19 02:27:27 +00:00
MaheshtheDev
149589ae7e fix(brain): keep the confirmed company domain after checkout return (#1536)
Returning from Stripe remounts onboarding and reseeds the domain from the user's email, so the header showed the wrong company and a research retry would re-run on the wrong domain. Past the confirm step, read the org's stored brainWorkspaceDomain instead.
2026-08-18 23:13:12 +00:00
MaheshtheDev
c0eb81c887 refactor(brain): one isCompanyBrainOrg helper in the web app (#1535)
Two hand-rolled copies of the add-on/brainMode rule replaced by a single shared helper, and the one-line isCompanyBrainOrganization wrapper dropped. No behaviour change.
2026-08-18 23:13:12 +00:00
ishaanxgupta
e2be9c9edd Fix integrations layout and mobile promo responsiveness (#1481)
## Summary

- Reorder Apps & extensions so Import X bookmarks appears in the top row and Apple Shortcuts uses the open space below.
- Keep both Apple Shortcut actions inline on larger screens while allowing the card to grow only as much as needed.
- Rework the Company Brain promo on phones so its logo, copy, close control, and CTA remain readable and aligned.
2026-08-17 18:47:36 +00:00
Dhravya
5d2b5855fe
feat(auth): AgentID sign-in button on the web login page (#1467)
## What?

Adds a "Continue with AgentID" button to the web app's login page, matching the existing Google/GitHub buttons (same `ExternalAuthButton` pattern, PostHog `login_attempt` capture, last-used badge).

- `packages/lib/auth.ts`: adds the `genericOAuthClient` plugin — generic OAuth providers sign in via `signIn.oauth2({ providerId })`, not `signIn.social`.
- `apps/web/app/(auth)/login/page.tsx`: the button, gated the same way as the other social buttons — always shown on cloud (`NEXT_PUBLIC_HOST_ID === "supermemory"`), opt-in elsewhere via `NEXT_PUBLIC_AGENTID_AUTH_ENABLED` (added to `.env.example`).

## Why?

Companion to supermemoryai/mono#2908, which registers an `agentid` generic OAuth provider (OIDC against auth.agentid.com) on the API so agents can authenticate with their AgentID identity. The consumer app talks to the same better-auth server, so it gets the same sign-in option. mono#2916 additionally auto-invites the agent's verified human owner to the agent's workspace.

Requires mono#2908 to be deployed for the button to work; until then the API rejects the unknown provider and the page shows its normal error state.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches authentication entry points and OAuth client configuration; risk is moderate because it extends login surface area but follows existing social sign-in patterns and is feature-flagged.
>
> **Overview**
> Adds **Continue with AgentID** on the web login page, using the same `ExternalAuthButton` flow as Google/GitHub (PostHog `login_attempt`, last-used badge, loading/error handling).
>
> The button calls **`signIn.oauth2({ providerId: "agentid" })`** instead of `signIn.social`, enabled by registering **`genericOAuthClient`** on the shared better-auth client in `packages/lib/auth.ts`.
>
> Visibility matches other social providers: shown on cloud when `NEXT_PUBLIC_HOST_ID === "supermemory"`, or elsewhere when **`NEXT_PUBLIC_AGENTID_AUTH_ENABLED`** is set (documented in `.env.example`). Depends on the API registering the `agentid` generic OAuth provider.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 90a32786a3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-08-16 23:20:38 +00:00
Dhravya Shah
d14b209f7c
feat(web): support discount code checkout (#1523) 2026-08-16 13:43:53 -07:00
Ishaan Gupta
e651045ac5
Remove paid plugin UI (#1403) 2026-08-15 18:41:46 +05:30
Dhravya
5ecbc26345
fix(mcp): surface real API error messages instead of 'restricted or blocked' (#1406)
## Why?

Plain **T-1554**: a user with a **read-only** MCP OAuth grant got 403s on memory listing, and the client rendered them as *"Access forbidden. Your account may be restricted or blocked."* The API's actual error body said `{"error": "This API key has read-only access"}` — but `handleError` discarded it, so the user (and support) chased a nonexistent account ban.

Two masking layers:
1. `handleError` used the raw error `message`, which for our raw-fetch endpoints was a hardcoded string ("Failed to fetch documents") or unparsed JSON, and fell back to the scary "restricted or blocked" text when empty.
2. `getDocuments` didn't read the response body at all.

## What?

- New `extractApiErrorMessage()` unwraps JSON error bodies (`{"error": ...}` / `{"message": ...}`) so the API's real reason reaches the user.
- `getDocuments` and `listMemoryEntries` now pass the (unwrapped) response body through with the status, letting `handleError` apply status-aware fallbacks when the body is empty.
- Reworded the empty-body 403 fallback to point at the common cause first: *"Access forbidden. This connection may be read-only or scoped to specific spaces — reconnect with broader access, or check your account status."*

Companion API-side fix (read-only grants couldn't call semantically-read POST list endpoints at all): supermemoryai/mono#2772.

## Testing

- Added tests: a 403 with a JSON error body surfaces the API's message; an empty-body 403 gets the scope-aware fallback. `vitest run src/server/client/index.test.ts` — 3 passed.
- `tsc --noEmit -p tsconfig.json` clean. (The `check-types` script also runs `tsconfig.widget.json`, which fails on origin/main with a pre-existing `UseAppOptions.strict` error, unrelated.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> User-facing error text only in the MCP client; no auth or API behavior changes.
>
> **Overview**
> **MCP client errors now show what the API actually returned** instead of hardcoded strings or misleading “restricted or blocked” text.
>
> Adds `extractApiErrorMessage()` to parse JSON bodies (`error` / `message` fields) from failed responses. **`getDocuments`** and **`listMemoryEntries`** read the response body on non-OK status and attach the unwrapped message (with status) for **`handleError`**, which also uses the helper on error messages. When a 403 has no body message, the fallback now points users toward **read-only or scoped OAuth** rather than an account ban.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1f492470cf4b619e58eea6d45af1dfa0b8cad0c4. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-08-14 22:36:52 +00:00
Prasanna721
9cbddcec56 docs: historical backfill guide (#1474)
Adds a focused guide for backfilling dated documents with `documentDate` and the batch ingestion API.

- includes TypeScript and Python batch examples plus optional completion polling
- links the guide from the docs navigation and ingestion entry points

Validated with `bunx mintlify@latest validate` and `bunx mintlify@latest broken-links`.
2026-08-14 20:46:21 +00:00
MaheshtheDev
2e85722cf4 Clarify Company Brain trial copy (#1469)
Make the trial terms and payment timing clear, and simplify the call to action.
2026-08-14 05:36:31 +00:00
Dhravya Shah
eac070048b
feat(web): add memory button to company brain navbar (#1468)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 22:04:01 -07:00
Abhinav Kumar Singh
82dae50ef4
fix(tools): bound memory forget requests (#1451) 2026-08-13 19:11:00 +05:30
Sarath Donepudi
7f448d55d8
docs: document pinned install for supermemory-server (#1238)
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
2026-08-13 17:56:49 +05:30
Abhinav Kumar Singh
9d64e0f950
fix: add type checks for TypeScript workspaces (#1447) 2026-08-13 17:55:50 +05:30
James Yang
1356affbd1
fix(extension): finish Included Memories leftovers on T3 (#1257) (#1421)
Co-authored-by: abhay-codes07 <abhaysingh0293@gmail.com>
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
2026-08-13 17:55:16 +05:30
Ishaan Gupta
fcf49855ce
Upgrade Nova model picker to current runtime models (#1404) 2026-08-13 17:08:50 +05:30
MaheshtheDev
0695ca421b feat(web): take a card before the Company Brain trial starts (#1459)
Onboarding now opens a trial step that collects a card through Stripe checkout before the brain is enabled, with a timeline showing today's $0, the day-12 reminder, and the day-14 charge.

- Only leaves the card step once the API confirms the trial is live
- Brain home shows a setup banner and dims what the trial unlocks
- Recovers orgs that abandoned checkout instead of stranding them
- Adds the organization ID to account settings, copyable from the label
2026-08-13 06:58:47 +00:00
Dhravya Shah
c70c142fc7
fix(web): open Slack install in new window (#1460)
Some checks failed
Publish OpenAI SDK Python / publish (push) Has been cancelled
2026-08-12 13:12:54 -07:00
Abhay Singh
b7a6ea9a5f
fix(extension): stop fragmenting Included Memories that contain commas or newlines (#1339)
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
2026-08-12 20:58:06 +05:30
Abhay Singh
00e57fb9c2
fix(web): stop formatUsageNumber rendering 1000.0K at unit boundaries (#1340)
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
2026-08-12 20:57:33 +05:30
pawan
47152afc1d
fix(openai-sdk): cap supermemory to <3.5 so a fresh install imports (#1236) 2026-08-12 20:56:54 +05:30
Abhay Singh
14bcc92c31
fix(memory-graph): center arrow-key navigation in the visible graph area (#1337) 2026-08-12 20:56:23 +05:30
Abhay Singh
74b2201eeb
fix(memory-graph): stop painting expired memories as expiring (#1335)
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
2026-08-12 20:55:53 +05:30
Abhay Singh
f163c932cf
fix(web): keep highlights card active index in range on refresh (#1334)
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
2026-08-12 20:55:01 +05:30
Abhay Singh
a7efd817ec
fix(validation): reject non-positive page/limit in pagination query schemas (#1271) 2026-08-12 19:35:45 +05:30
MaheshtheDev
59b148e5b2 feat(web): sell Company Brain on Max as well as Scale (#1440)
Company Brain workspaces could only buy Scale at $399/mo, which is roughly eight times what the median team uses. Adds the $100/mo Max card to the Company Brain plan picker, notes what a Scale trial loses on the way down, and flags that Scale is cheaper above about $400/mo of credits.
2026-08-10 00:10:46 +00:00
MaheshtheDev
be267c2fc8 feat(web): automation connection warnings and calmer automations page (#1396)
Inline notice with app icons when a channel automation can't use personal-only connections (footer, next to Save), post-save warning toast from the API, templates capped to 3 connection-relevant ideas with a show-all toggle, and New automation promoted to a primary button on the heading row. Pairs with mono #2724; degrades gracefully without it.

Fixes ENG-1151
2026-08-10 00:02:50 +00:00
vorflux[bot]
2731de5c06
fix(web): add Gmail connector logo (#1428)
Co-authored-by: Vorflux AI <249966464+vorflux[bot]@users.noreply.github.com>
2026-08-07 17:35:26 -07:00
Dhravya Shah
45585b4c0f
feat(web): add API Keys management in settings (#1426)
Co-authored-by: Mahesh Sanikommu <maheshthedev@gmail.com>
2026-08-07 12:44:40 -07:00
Prasanna721
bf81cb94ea add ChatGPT default memory FAQ (#1423)
Adds one FAQ to the ChatGPT Web setup guide for making Supermemory the default memory.

- shows how to disable ChatGPT memory and optionally move the existing summary
- adds light and dark screenshots plus the custom instruction to paste

Tested with `mintlify validate` and `mintlify broken-links`.
2026-08-07 01:14:03 +00:00
sohamd22
069b8c373c docs(memories): forget-matching accepts an id list (bound preview→apply) (#1367)
### TL;DR

Documents the new `ids` parameter for the `forget-matching` endpoint, allowing exact memory deletion without semantic search.

### What changed?

The `forget-matching` endpoint now accepts either a `query` (semantic search-based forgetting) or an explicit `ids` list (direct deletion by memory ID) — one or the other must be provided. The docs have been updated to reflect this:

- The `query` parameter is now marked as `one of*` rather than required, and `ids` is introduced as an alternative with the same mutual-exclusivity constraint.
- `threshold` is clarified as applying to `query` mode only.
- Code examples for both JavaScript and cURL now show the recommended two-step pattern: run a `dryRun` with `query`, then apply using the `ids` returned from the preview — avoiding drift if the container changes between steps.
- A new `<Tip>` block explains why passing `ids` on the apply step produces a more deterministic delete than re-running the `query`.

### How to test?

1. Call `forget-matching` with `dryRun: true` and a `query` to retrieve candidate `id`s.
2. Re-call with `dryRun: false` and the `ids` from step 1 to confirm only those exact memories are forgotten.
3. Verify that passing `ids` belonging to a different `containerTag` are ignored.
4. Confirm that providing both `query` and `ids`, or neither, returns an appropriate validation error.

### Why make this change?

Re-running a `query` on the apply step can produce different results if the container was modified between the preview and the apply. Exposing `ids` as a first-class parameter lets callers pin the delete to exactly the set they reviewed, making bulk forgetting safer and more predictable.
2026-08-06 09:32:32 +00:00
MaheshtheDev
73d15ac9f8 feat(web): add API key and extra headers to custom MCP dialog (#1419)
Header-auth MCP servers like Plane need an API key plus a second header,
which the custom connector dialog had no way to collect.

- Restructured on the Claude connector pattern: name + URL up front,
  credentials behind collapsed Advanced settings
- Adds API key, header name, and repeatable extra header rows
2026-08-05 20:02:22 +00:00
sreedharsreeram
570ed22b6c feat(skills): surface Company Brain-created skills (#1412)
## What and why

Make skills created through Company Brain visible in the existing Settings catalog without a manual reload. The UI labels Slack-originated skills and refreshes the catalog while the Settings page is open.

```mermaid
flowchart LR
  A[Slack approval] --> B[Company Brain skill catalog]
  B --> C[Skills API query]
  C --> D[Settings Skills list]
  D --> E[Created by Company Brain label]
```

## Validation

- `bunx biome check apps/web/components/settings/company-brain-skills.tsx apps/web/components/settings/company-brain-skills/domain.ts apps/web/components/settings/company-brain-skills/skill-row.tsx apps/web/hooks/use-brain-skills.impl.ts` — passed

## Impact

Settings polling refreshes every 15 seconds and on window focus. It does not change the Nova/browser chat workflow or create skills from that surface.
2026-08-05 03:42:37 +00:00
Prasanna721
4d86b728dd update supermemory mcp docs (#1408)
Updates the Supermemory MCP docs for the revamped tool, space, widget, and OAuth flows.

- adds a screenshot-backed ChatGPT Web setup guide with light and dark variants
- refreshes the overview, setup, tools, spaces, and widget docs
- keeps manual JSON configuration in a dropdown

Tested all four local MCP docs routes and image references.

for mcp image light mode -> dark mode images and dark mode has light mode (since the background was blending in i made this way to refocus user attention on the screenshots)
2026-08-04 19:58:49 +00:00
Prasanna721
a99cf4f7e1 fix mcp graph and file uploads (#1397)
Fixes cross-host graph rendering and moves widget uploads off the JSON/base64 tool transport.

- render graph data from the launcher result without a second tool call
- stream multipart uploads through one-time, short-lived upload sessions
- remove temporary widget diagnostics and redundant unit tests

Tested with Biome, TypeScript, 17 unit tests, a Wrangler deployment dry-run, and live graph rendering in ChatGPT and Claude. Authenticated E2E setup is currently blocked by the saved OAuth refresh session returning `invalid_grant: session not found`.
2026-08-04 18:58:14 +00:00
Prasanna721
434c86e2c3 fix docs image pipeline (#1411)
Keep docs asset URLs repo-root-relative and pass images as literal MDX children so Mintlify can compile them through OptimizedImage under the /docs mount.

Mintlify's official guidance says: “Image paths are root-relative from your docs repository.” It also says relative paths such as `./screenshot.png` are unsupported. See [Image embeds](https://www.mintlify.com/docs/create/image-embeds).

The existing `/images/...` paths were correct. The failure came from passing them through custom-component string props, which kept Mintlify from seeing those images during its MDX transform.

- fix hero, building-block, and Slack avatar images
- preserve Slack avatar clipping
- normalize Hermes and Company Brain icon sizing

Tested with Mintlify validation, broken-link checks, and browser checks across every changed route.
2026-08-04 04:55:50 +00:00
Prasanna721
05aab3a09f fix docs links and assets (#1407)
Fixes broken docs navigation and asset paths from the site audit.

- serves docs images from `/images` and adds the missing Cartesia icon
- corrects homepage, console, and LinkedIn destinations
- removes agent-only comparison headings from the web TOC

Tested with Mintlify validate and Mintlify broken-links.
2026-08-04 00:04:41 +00:00
MaheshtheDev
a787041ca7 feat(web): company brain trial visibility + setup timeline (#1384)
- Header pill with trial days left (Autumn-first, org metadata fallback)
- Brain home: Your Company Brain timeline card (trial strip, milestones from /brain/overview) promoted to top-right
- Trial copy in CB onboarding Slack step and docked header
- Brain home now reads the new /brain/overview endpoint (drops the dead /brain/connections fetch)

Fixes ENG-1142
2026-08-02 07:46:05 +00:00
sreedharsreeram
f14cdd7a4c feat(web): add Company Brain skills settings (#1322)
## Stack Context

This is the frontend half of the Company Brain Skills feature. The harness is implemented in supermemoryai/mono#2611.

## What?

Add Skills settings with separate Org-wide and Personal sections, Markdown upload autofill, scoped creation and editing, approval controls, and server-driven permissions.

## Why?

Members need a focused way to manage their private playbooks while admins create and approve organization-wide guidance.

## Related

- Harness: https://github.com/supermemoryai/mono/pull/2611

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/d8c77451-8f55-47ab-a182-5c98da616263)
- Requested by: Sreeram Sreedhar (sreeram@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-08-02 05:36:32 +00:00
DisturbedCrow
af9c6b74e8
fix(docs): correct ingestion queue anchor (#1329) 2026-08-01 17:37:03 +05:30
Abhay Singh
219cb64c82
feat(extension): expand t.co links when importing tweets (#1352) 2026-08-01 17:35:33 +05:30
Abhay Singh
785c96e682
fix(extension): import the best-quality Twitter video variant (#1351) 2026-08-01 17:34:57 +05:30
Prasanna721
7e7e820489 fix mcp app contracts (#1394)
Fixes MCP app submission metadata and makes widget delivery consistent across hosts.

- content-hash widget resources and set the production widget domain
- add typed structured outputs to direct tools and scope default-space calls
- keep graph rendering compatible with both initial results and app-side loading

Tested with `bun run check-types` and `bun run test:unit` (42 tests).
2026-08-01 06:41:17 +00:00
Prasanna721
68769200b0 validate MCP OAuth login flow (#1395)
Adjusts login handling for a limited credential-validation flow. Normal Google and magic-link sign-ins are unchanged for other accounts.

Tested with direct login validation and a production build.
2026-08-01 05:15:22 +00:00
Prasanna721
48fb1969f8 fix chatgpt memory graph rendering (#1393)
ChatGPT can omit nullable nested fields from structured tool results and does not reliably expose result metadata to MCP Apps. Keep graph data in `structuredContent`, tolerate missing document titles, and bundle React inside the widget so the graph renders consistently across hosts.

- use the deployable `app-v4` resource URI
- keep the graph response and widget schema aligned
- remove the `esm.sh` runtime dependency

Tested with typecheck, 42 unit tests, production widget build, 36 branch-local authenticated E2E tests, and a live ChatGPT graph render.
2026-07-31 21:20:15 +00:00
Prasanna721
9e194fbc50 fix MCP app result contracts (#1385)
Moves MCP App results onto explicit runtime schemas so hosts can distinguish model-visible output from widget-only graph data.

- advertises output schemas for structured tools
- validates API, session, and widget boundaries instead of asserting response types
- marks additive app writes as non-destructive and removes redundant widget typing

Verified with MCP typecheck, 42 unit tests, and the production widget build. Claude Desktop host validation is in progress against a temporary tunnel.
2026-07-31 18:23:29 +00:00
MaheshtheDev
e8ed80f768 feat(web): Slack disconnect UI and connect-error toasts (#1375)
Part of ENG-1136
2026-07-31 07:11:26 +00:00
MaheshtheDev
b21ea31ffc fix(web): rework agents discover panel in Select Space modal (#1379)
Agent switcher snapped back to the just-connected plugin while the API-key banner was open; fixed the effect so manual tab clicks stick. Replaced the double header + blurred install-steps overlay with named chip tabs (matching existing chip style), inline Connect + Docs, and a plain step-title preview.

Fixes ENG-1141
2026-07-31 04:52:36 +00:00
Dhravya Shah
a7161c7132
fix(mcp): add missing tool safety annotations (#1383) 2026-07-30 21:10:42 -07:00
Dhravya Shah
ff40a82d69
Add .well-known/openai-apps-challenge route to MCP server (#1382) 2026-07-30 21:06:14 -07:00
Prasanna
04e5b4dccf
fix clipped space picker (#1381) 2026-07-30 19:15:23 -07:00
Dhravya Shah
19e8f06cf1
MCP Revamp (#1120) (#1380)
Co-authored-by: Prasanna <106952318+Prasanna721@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: ved015 <vedant.04.mahajan@gmail.com>
Co-authored-by: ved015 <ved015@users.noreply.github.com>
Co-authored-by: Ishaan Gupta <ishaankone@gmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-30 17:08:51 -07:00
Om Shah
d4377b1609
Add consent flow for Slack workspace reassignment (#1369)
Co-authored-by: Vorflux AI <249966464+vorflux[bot]@users.noreply.github.com>
Co-authored-by: Mahesh Sanikommu <maheshthedev@gmail.com>
2026-07-30 16:42:15 -07:00
Prasanna721
95a34602b7 register SpaceState durable object (#1376)
Registers `SpaceState` before the stateless MCP starts routing requests through it. The class is intentionally unused here, so this changes no request behavior.

Rollout:
1. Apply this exact commit once with `wrangler deploy`, not `wrangler versions upload`.
2. Rerun the Workers check and merge this parent PR.
3. Land #1120, which adds the active-space methods and request routing.

Verified with the MCP widget build, isolated class typecheck, and Wrangler deploy/version dry runs.
2026-07-30 21:28:22 +00:00
MaheshtheDev
a051ba0e28 feat(web): give Configure sections real routes under /configure (#1378)
Configure sections were local useState, so they could not be linked or bookmarked and always opened on the default section. Each section is now a route (/configure, /configure/models, /configure/workspace-prompt, /configure/proactivity, /configure/automations), mirroring the existing /integrations/[card] pattern. The shell renders from the segment layout so switching sections does not remount the app, and legacy /?view=configure links forward to /configure.

Fixes ENG-1140
2026-07-30 19:27:36 +00:00
MaheshtheDev
33e927417f feat(web): search channels in proactivity exceptions picker (#1377)
Workspaces with hundreds of Slack channels made the plain select unusable, so the channel exception picker is now a Popover + Command combobox that filters as you type. The exceptions block is also reworked into one list card with an inline add row, matching the max-w-3xl measure used by the workspace prompt pane.

Fixes ENG-1138
2026-07-30 18:08:16 +00:00
MaheshtheDev
1034e337ba feat(web): Company Brain proactivity settings UI (#1374)
Part of ENG-1135
2026-07-29 21:47:29 +00:00
vorflux
8071a7b085 Add Nova workspace prompt settings (#1323)
Adds a dedicated Workspace Prompt editor for Company Brain organizations while preserving the existing Organization Context ingestion-filter controls for every organization manager.

## Changes

- Keeps Organization Context byte-for-byte unchanged and available independently to all organization managers.
- Adds Workspace Prompt as a separate Company-Brain-only section below it, using the established settings styling and contextual divider.
- Describes Workspace Prompt as persistent guidance that can shape operating preferences, priorities, source/tool choices, workflows, terminology, formatting, and communication style.
- Adds nullable, 1,500-character `workspacePrompt` support to shared request, GET response, and PATCH response contracts.
- Aligns PATCH validation with the real `{ orgId, orgSlug, updated }` API response.
- Merges canonical `updated` settings into the submitting organization’s cache, then exactly refetches that organization.
- Preserves drafts during background refetches, isolates organization switches, retains actionable errors, accessibility, empty `filterPrompt` compatibility, and `X-App-Source: nova`.

## Testing

- Passed focused Biome checks on all changed files.
- Passed `packages/lib` and `packages/validation` TypeScript checks.
- Verified GET/PATCH settings response contracts, partial/null/limit validation, canonical cache merge, and exact organization-bound invalidation.
- Verified Organization Context remains unchanged and Workspace Prompt is separately Company-Brain/manager-gated.
- Confirmed no remaining Workspace Persona identifiers.
- Public preview returns HTTP 200; authenticated settings interactions remain unavailable without a saved OAuth session.
- Full web type-check remains blocked by unrelated baseline diagnostics; none reference changed files.
- No dedicated tests were added, per requester instruction.

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/7544b72b-aeca-48e2-81c3-514df21cd081)
- Requested by: Soham Daga (soham@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-07-29 21:13:59 +00:00
ishaanxgupta
5fa0535a64 Render Nova connector setup cards (#1071)
## Summary
- Add custom assistant-message rendering for Nova connector tool results
- Show integration-style setup/status cards with icon, status pill, setup steps, docs links, copy buttons, and key reveal/generate action
- Keep plugin API keys client-side only by calling the existing `/v3/auth/key?client=...` endpoint from the UI
- Add Nova empty-state suggestions for Cursor setup and active plugins
2026-07-28 15:58:50 +00:00
MaheshtheDev
db7f5c3f64 fix(web): select the correct Company Brain workspace (#1372)
## What changed

- Make `/brain` reuse the active Company Brain, switch to a single existing Company Brain, show a picker for multiple choices, or create one only when none exists.
- Wait for active-organization restoration before making that decision.
- Make Company Brain onboarding match organizations by the confirmed company domain and never offer unrelated-domain workspaces.
- Create a new Company Brain when no matching workspace exists and show an actionable workspace-limit toast when creation is blocked.
- Improve the organization picker, loading, and error states.

## Why

The previous flows could use or mutate the currently active normal organization, start research against stale Company Brain metadata, or offer unrelated Company Brain workspaces after a quota failure.

## Impact

Normal organizations are no longer silently converted. Research is scoped to the Company Brain for the confirmed domain, and users receive a clear recovery path when they reach their workspace limit.

This keeps the existing `{ domain }` research API contract; no organization-reconfiguration API is required.

## Validation

- Biome checks passed for all five changed web files
- Targeted web TypeScript diagnostics reported no errors for the changed files
- React Doctor against `origin/main`: no issues found
- `git diff --check`

No test files were added.
2026-07-28 06:14:22 +00:00
MaheshtheDev
ac880a4dc6 feat(web): surface Company Brain to personal-brain users (#1370)
Adds a dismissible Company Brain card to the dashboard header slot and a permanent entry in the profile menu for users whose org has no company brain, both linking to team onboarding.

Onboarding now honours ?mode=team so a personal-domain email arriving from those CTAs isn't routed to personal onboarding.

Fixes ENG-1132
2026-07-28 04:25:04 +00:00
Dhravya Shah
8a352dca81
fix(web): allow custom MCP connections (#1371) 2026-07-27 20:34:45 -07:00
Dhravya Shah
fa7588c43e fix 2026-07-26 14:45:47 -07:00
Nolan Selby
7e182fca9a
fix(web): update ChatGPT MCP setup instructions (#1358)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 13:06:25 -07:00
ved015
6f3c835e8f feat(web): add Cursor to Agents (#1361)
Adds Cursor projects to Agents spaces with source filters, legacy labels, icons, and Codex-style structured conversation rendering.

Tests: targeted Agents and plugin-document tests.
2026-07-25 22:09:28 +00:00
Dhravya
80af8c9043
fix(web): use organization branding in Company Brain shares (#1360)
<!-- VORFLUX_AGENT_PR_BODY_BEGIN -->
Company Brain share snapshots now use the active organization’s branding instead of the signed-in user’s personal name.

## Changes

- Detect Company Brain workspaces through the canonical hook.
- Render the normalized organization name with `Company Brain`, preserving personal share branding for other workspaces.
- Handle missing, organization-suffixed, and already-possessive organization names.

## Testing

- **Passed:** `node_modules/.bin/biome ci apps/web/components/share-modal.tsx`
- **Passed:** `git diff --check -- apps/web/components/share-modal.tsx`
- **Baseline failure:** `node_modules/.bin/tsc --noEmit --incremental false -p apps/web/tsconfig.json` reports existing workspace diagnostics. The only diagnostic in `share-modal.tsx` is present on `HEAD`; none reference the new branding code.
- **Blocked:** authenticated visual verification was not performed because no authenticated browser state was available and no login retry was requested.
  - Blocker screenshot:
  - Blocked-flow recording:

---
**Attached Images and Videos**

![share-personal.png](https://api.us1.vorflux.com/assets/artifacts/c3VwZXJtZW1vcnk6Zjo3NDI0.XIMCcXoYysALm_LIfpnvO2S9fa6ME5753QqP6-8JZQE.png)

🎥 [View recording: share-branding-walkthrough.webm](https://api.us1.vorflux.com/assets/artifacts/c3VwZXJtZW1vcnk6Zjo3NDI1.PLlwgZToeGPRdV49tEQESgrTiADIUq5pHQcQjzfGMQ0.mp4)
<!-- VORFLUX_AGENT_PR_BODY_END -->

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/22b72a15-9127-44be-b15b-3d341e26d016)
- Requested by: Dhravya Shah (dhravya@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Copy-only UI in the share preview with no auth, API, or data-handling changes.
>
> **Overview**
> Share snapshot previews now switch branding when the workspace is **Company Brain**, instead of always showing the signed-in user’s name and **supermemory**.
>
> The modal uses `useHasCompanyBrain` and org data from auth to set the preview header: a normalized org possessive label (with fallbacks for missing names, trailing “organization”, and names that already end in `'s`) plus **Company Brain** as the product line. Non–Company Brain workspaces keep the existing personal **supermemory** branding.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 087019f357b8c9c028bd2e9e915a550bcce24573. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-07-25 03:46:17 +00:00
MaheshtheDev
c7dc49decf feat(web): add Slack account linking confirmation (#1359)
## Stack Context

This is the Nova UI half of Slack account linking. The paired API and database work is [mono#2650](https://github.com/supermemoryai/mono/pull/2650).

## What?

- Preview the Slack and signed-in Supermemory identities before linking.
- Block accounts that are not already organization members.
- Require explicit confirmation when replacing an existing mapping.
- Handle account switching and invalid, expired, used, success, and retry states.

## Why?

Users whose Slack and Supermemory emails differ need a clear, secure way to confirm their identity. The page makes the identities and organization membership requirement explicit before creating or replacing a stable mapping.
2026-07-24 22:00:58 +00:00
MaheshtheDev
4aa044fe55 fix(mcp): respect readable scope for unscoped recall (#1357)
Unscoped recall forced sm_project_default even when the caller could only read another organization space, causing a misleading 403 while list and graph operations succeeded. Let the search API choose the caller's readable scope, skip profile enrichment when no concrete scope is selected, and retain upstream error details.

Validated with Biome, Vite production build, and Wrangler deploy dry-run.
2026-07-24 19:01:51 +00:00
Vedant Mahajan
e685762012
Add OpenCode to Agents spaces (#1354)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-07-24 08:51:19 -07:00
MaheshtheDev
ea2cf33fd3 fix(web): invite dialog focus trap inside settings modal (#1347)
Portal the invite-teammate dialog into the settings modal and autofocus the email input, matching the delete-org dialog fix. Stacked body-portaled dialogs broke focus so the email field stopped accepting input after the first invite.

Fixes ENG-1110
2026-07-24 03:18:39 +00:00
Vedant Mahajan
cfc2b49192
Add shared Agents memory workspace (#1290) 2026-07-23 12:18:44 -07:00
MaheshtheDev
5a3ff85ea5 fix(web): don't use consumer email domains as the company domain (#1343)
workspaceDomainFromEmail returned the raw domain for free providers, so a gmail.com user picking Team was shown gmail.com as their company domain. Confirming it named the org "Gmail", persisted brainWorkspaceDomain, and ran company research against Gmail.

It now returns null for consumer providers, so the confirm step starts empty and requires a real company domain.
2026-07-22 23:42:03 +00:00
MaheshtheDev
3c3accaab9 feat(web): open company brain signup to everyone (#1342)
- Drop the company-brain-beta PostHog flag gate; Team mode is available to all users.
- Remove the invite-only gate card and the effectiveMode downgrade that silently wrote brainMode: "personal" on team signups.
- Add a "Use a personal workspace instead" link to the domain-confirm step, which had no way back once the gate stopped catching users.

Fixes ENG-1106
2026-07-22 21:05:53 +00:00
Dhravya Shah
72431e9afc
fix(docs): revert homepage href prefixes causing doubled /docs/docs/ 404s (#1341)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 13:24:17 -07:00
Abhay Singh
d5f95827ed
fix(memory-graph): allow selecting nodes on touch devices (#1262) 2026-07-22 17:57:50 +05:30
Abhay Singh
fa16e853a8
fix(extension): paginate Twitter bookmark-folder imports past the first page (#1269) 2026-07-22 17:54:14 +05:30
Dhravya Shah
90f709bf8d
fix(docs): prefix raw internal href/src literals with /docs (#1333)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 00:40:16 -07:00
Dhravya Shah
fa0823a300
fix(docs): prefix JSX-prop image paths with /docs to fix 404s (#1332)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 20:27:44 -07:00
Dhravya Shah
f882f1104d
docs: restructure documentation site and update integration UI (#1331)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-21 20:19:30 -07:00
Dhravya Shah
816b85d756
fix(mcp): annotate tool safety hints for ChatGPT (#1330) 2026-07-21 14:45:45 -07:00
MaheshtheDev
9bbe436fab docs(brain): note trial attach after Slack OAuth (#1313)
## Summary
- Documents that mono attaches Scale 14d trial + Company Brain after Slack OAuth from `/brain`.

Stacks on #1310. Billing implementation is in mono.
2026-07-21 21:34:25 +00:00
MaheshtheDev
e39ba92cb4 feat(brain): add /brain Slack-first onboarding entry (#1310)
New /brain route creates the org straight from signup and redirects into the Slack install, so onboarding has no domain-confirmation step. Returning from OAuth shows an Open Slack handoff instead of a toast.

Also models the terminal research error state: polling stops, the UI retries once, and the docked header no longer gates Continue on research finishing.
2026-07-21 21:19:54 +00:00
Ishaan Gupta
2426305a2d
feat(extension): Add support across gemini & claude, live tool tip, auto complete support (#971)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-07-21 13:12:20 -07:00
Rajarshi Datta
86c3ad69d1
fix(middleware): update deduplication logic for profile memories in query mode (#1243) 2026-07-21 19:25:32 +05:30
871 changed files with 30685 additions and 107970 deletions

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

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

View file

@ -26,8 +26,83 @@ jobs:
- name: Install dependencies - 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

View file

@ -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 }}"
}
}
}
}'

View file

@ -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.

View file

@ -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 }}"
}
}
}
}'

View file

@ -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/

View file

@ -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

View file

@ -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/

View file

@ -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/

View file

@ -7,7 +7,7 @@
</p> </p>
<p align="center"> <p align="center">
<strong>State-of-the-art memory and context engine for AI. 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 |

View file

@ -7,7 +7,7 @@
</p> </p>
<p align="center"> <p align="center">
<strong>面向 AI 的记忆与上下文引擎,业界领先。也可以把它当作公司或个人的「大脑」来用。</strong> <strong>面向 AI 的记忆与上下文引擎,业界领先。</strong>
</p> </p>
<p align="center"> <p align="center">
@ -60,7 +60,7 @@ Supermemory 是为 AI 设计的记忆与上下文层。在 **[LongMemEval](https
<h3>🧑‍💻 我只是 AI 工具的用户</h3> <h3>🧑‍💻 我只是 AI 工具的用户</h3>
直接用我们的应用,给自己搭一份专属的 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 agentSupermemory 作为记忆 providerhttps://github.com/NousResearch/hermes-agent - Hermes agentSupermemory 作为记忆 providerhttps://github.com/NousResearch/hermes-agent
### MCP——一键安装 ### MCP
```bash 服务地址:
npx -y install-mcp@latest https://mcp.supermemory.ai/mcp --client claude --oauth=yes
```text
https://mcp.supermemory.ai/mcp
``` ```
`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()` | 配置记忆抽取与切分策略 |

View file

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

View file

@ -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?

View file

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

View file

@ -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>
)
}

View file

@ -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
}
},
)
})

View file

@ -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,
)
}

View file

@ -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)
}

View file

@ -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")
}
}

View file

@ -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()
}
},
})

View file

@ -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
}
}
})
}

View file

@ -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)
}

View file

@ -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)
})
}
}

View file

@ -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");
}

File diff suppressed because it is too large Load diff

View file

@ -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>

View file

@ -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>,
)
}

View file

@ -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;
}

View file

@ -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

View file

@ -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>

View file

@ -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>,
)
}

View file

@ -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;
}

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7 KiB

View file

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

View file

@ -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
}
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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,
},
},
})

View file

@ -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),
})
}

View file

@ -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
}
}

View file

@ -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",
})
})
})

View file

@ -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 } : {}),
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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 += `![Image ${index + 1}](${photo.url})\n`
})
markdown += "\n"
}
if (tweet.videos && tweet.videos.length > 0) {
markdown += "**Videos:**\n"
tweet.videos.forEach((video, index) => {
markdown += `[Video ${index + 1}](${video.url})\n`
})
markdown += "\n"
}
// Add hashtags and mentions
if (tweet.entities.hashtags.length > 0) {
markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n`
}
if (tweet.entities.user_mentions.length > 0) {
markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n`
}
// Add raw data for reference
markdown += `\n---\n<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`
return markdown
}
/**
* Build Twitter API request variables for pagination
*/
export function buildRequestVariables(cursor?: string, count = 100) {
const variables = {
count,
includePromotedContent: false,
}
if (cursor) {
;(variables as Record<string, unknown>).cursor = cursor
}
return variables
}
/**
* Build Twitter API request variables for bookmark collection
*/
export function buildBookmarkCollectionVariables(bookmarkCollectionId: string) {
return {
bookmark_collection_id: bookmarkCollectionId,
includePromotedContent: true,
}
}

View file

@ -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[]
}

View file

@ -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
},
}

View file

@ -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"],
},
})

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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"] }
```

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

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

View file

@ -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>

View file

@ -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>

View 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

View 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)

View 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)

View 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>

View 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)

View 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)

View 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)

View 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)

View 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)

View file

@ -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 | — | 1365 days | | `expiresInDays` | No | — | 1365 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>

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -1,6 +1,6 @@
--- ---
title: "Container Tags" title: "Container Tags"
sidebarTitle: "Container Tags" sidebarTitle: "Container tags"
description: "The isolation boundary that groups and partitions memories by user, project, or any logical scope" description: "The isolation boundary that groups and partitions memories by user, project, or any logical scope"
icon: "folder" icon: "folder"
--- ---
@ -32,7 +32,7 @@ await client.add({
}); });
// Later, retrieve only Alex's memories // Later, retrieve only Alex's memories
const results = await client.search.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>

View file

@ -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">

View file

@ -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">

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