Compare commits

...

199 commits

Author SHA1 Message Date
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
vorflux[bot]
566be20898
Update Gmail connector plan requirement docs (#1311)
Co-authored-by: Vorflux AI <249966464+vorflux[bot]@users.noreply.github.com>
2026-07-18 16:28:40 -07:00
sreedharsreeram
9824f0c9cd feat(web): configure Company Brain reasoning effort (#1307)
## Stack context

Stacks on #1306, which moves Company Brain settings into the revamped Configure page.

Backend contract and runtime support: supermemoryai/mono#2581. The UI safely hides any effort controls omitted by an older backend response.

## What changed

- Adds independent Low, Medium, High, and Extra high reasoning controls for Main, Triage, and Research.
- Saves model and reasoning edits together through the existing partial PATCH.
- Keeps controls visible but disabled for non-admin members.
- Explains that Extra high maps to High for Grok and GPT providers.

## Validation

- `bunx biome check apps/web/hooks/use-brain-models.ts apps/web/components/settings/company-brain-models.tsx`
- `bun run build` in `apps/web`
- `git diff --check`

The standalone web TypeScript command still reports pre-existing unrelated errors outside these files.

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/1c4f3ec7-a536-4105-bbe7-8b19e61f245f)
- Requested by: Sreeram Sreedhar (sreeram@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-07-18 19:12:45 +00:00
MaheshtheDev
20e585cd00 feat(web): revamp Company Brain configure page (#1306)
- New Configure tab with sidebar sections: Integrations, Models, Automations
- Merge org/personal connection tabs into one grid: per-card scope chips, admin scope dropdown, Slack as a card, custom MCP via modal
- Automations: always-visible template cards with dashed blank-create tile
- Home: Connect your tools only shows unconnected apps

Fixes ENG-1075
2026-07-18 18:45:03 +00:00
Abhay Singh
7c848a5da3
fix(web): declare entityContext in bulkLinkMutation so bulk link import works (#1260) 2026-07-18 19:15:59 +05:30
Paramveer singh
bec73e28ad
docs(tools): correct addMemory default in OpenAI middleware JSDoc (#1253) 2026-07-17 23:17:26 -07:00
Abhay Singh
86854efee6
fix(tools): make claude-memory file operations act on the exact file, literally (#1285)
Some checks failed
Publish Tools / publish (push) Has been cancelled
2026-07-17 18:24:09 +05:30
Abhay Singh
d8796277b0
fix(agent-framework): restore importability on current agent-framework-core releases (#1281) 2026-07-17 14:49:10 +05:30
Dhravya Shah
82c03a87ce
docs: drop Pro-plan banner for Claude Code & OpenCode (now free-tier) (#1298)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 19:38:08 -07:00
Dhravya Shah
400e2f4d7d
docs(readme): surface latest research numbers front and center (#1297)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:58:10 -07:00
ishaanxgupta
8d926332ab company brain responsiveness (#1239)
## Summary

- Improves the Company Brain header and stats cards on mobile without changing the desktop layout intent.
- Tightens the mobile Slack banner so the Slack logo is not duplicated and the card stays compact.
- Centers the onboarding step flow and card as one stack while keeping the logo independent.

before:
<img width="388" height="864" alt="image" src="https://github.com/user-attachments/assets/f02890e4-78d1-45cf-a819-97306df20d5c" />

after:
<img width="193" height="431" alt="image" src="https://github.com/user-attachments/assets/1b5d6907-1dc1-4d95-9b4f-2b40defecda5" />
<img width="391" height="865" alt="image" src="https://github.com/user-attachments/assets/67fb6009-bace-4c78-9b72-b4dfcdd79b90" />
<img width="385" height="865" alt="image" src="https://github.com/user-attachments/assets/f3c3d785-762e-49c2-92bb-461358eda418" />
<img width="394" height="865" alt="image" src="https://github.com/user-attachments/assets/f5943779-a141-467d-a350-80678f3a2513" />
2026-07-16 01:57:51 +00:00
MaheshtheDev
ef0026a23c feat(brain): Company Brain Models settings tab (#1292)
Adds an admin-gated Models tab (main/triage/research pickers) that reads/writes the mono /brain/models endpoint, shown only for Company Brain orgs. Extracts a shared useOrgMemberRole hook so the brain settings sections dedupe the getActiveMember call.

Fixes ENG-1054
2026-07-14 18:56:01 +00:00
Gautam Sharma
2cebe81512
fix(web): bypass auth proxy for local dev (#1213)
Some checks failed
Publish Tools / publish (push) Has been cancelled
Co-authored-by: Dhravya Shah <dhravya@supermemory.com>
Co-authored-by: Dhravya Shah <dhravyashah@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 20:11:48 -07:00
Gautam Sharma
19d7f122b2
fix(web): use backend URL fallback for direct fetches (#1212)
Co-authored-by: Dhravya Shah <dhravyashah@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 20:02:24 -07:00
Haroon
e5e4e498f2
fix(tools): coerce limit/offset in ai-sdk schemas (#1202)
Co-authored-by: Dhravya Shah <dhravyashah@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:42:15 -07:00
Dhravya Shah
5567e82377
docs(self-hosting): configurable embeddings for Supermemory local (#1210)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-10 19:15:46 -07:00
Mrityunjay Raj
453185abf3
ci: add concurrency groups to workflows (#1221)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 19:15:32 -07:00
Gautam Sharma
6763c36d61
fix(extension): scope related memory search (#1215) 2026-07-10 19:15:29 -07:00
Gautam Sharma
bc6eb73d43
fix(tools): reject conflicting container config (#1214) 2026-07-10 19:15:25 -07:00
Bharath
09e3b78fc2
perf(lib): hoist customAlphabet factory in generateId (#1204) 2026-07-10 19:15:21 -07:00
Haroon
d77b6eb6d9
feat(memory-graph): configurable labels + popover z-index (#1201) 2026-07-10 19:15:17 -07:00
Abhay Singh
9487a928f6
feat(mcp): add listMemories tool for enumerating stored memories (#1183) 2026-07-10 19:15:12 -07:00
Abhay Singh
b49903a493
fix(web): make the finished-processing refresh effect actually fire (#1188) 2026-07-10 19:15:02 -07:00
Abhay Singh
72ab99a77b
fix(web): stop extractUrls treating email addresses as URLs (#1187) 2026-07-10 19:14:57 -07:00
Abhay Singh
95fc201072
fix(web): stop truncating multi-line plugin transcript messages (#1185) 2026-07-10 19:14:53 -07:00
pawan
942f03eef9
fix(cartesia-sdk): treat empty profile as no memories, not a retrieval error (#1164) 2026-07-10 19:14:49 -07:00
Dhravya Shah
36b07e2a26
chore(tools): bump @supermemory/tools to 2.1.0 (#1234)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:55:15 -07:00
Sandipan kundu
2163f592b7
fix(tools,validation): persist tool-call turns in conversation memory & tidy search thresholds (#1211)
Co-authored-by: Dhravya Shah <dhravyashah@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:54:56 -07:00
Abhay Singh
501613b6bc
fix(tools): repair SDK call mismatches that broke five tools (#1186) 2026-07-10 18:30:41 -07:00
Sepuri Sai Krishna
67b6585346
fix(web): block IPv6 literals in OG scraper SSRF guard (#1154) 2026-07-10 18:20:43 -07:00
Abhay Singh
c00f3e1b22
fix(web): match YouTube URLs by hostname instead of substring (#1182) 2026-07-10 16:20:18 -07:00
Mrityunjay Raj
59a0147296
ci: add timeout-minutes to all workflow jobs (#1223) 2026-07-10 15:48:24 -07:00
Mrityunjay Raj
7785420eb0
ci: skip Claude review on fork PRs (#1231)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 15:46:25 -07:00
Mrityunjay Raj
398737fadc
ci: pass workflow_run id to github-script via env (#1227) 2026-07-10 15:22:23 -07:00
Mrityunjay Raj
f994180022
ci: gate @claude workflow triggers by author association (#1229)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 15:10:29 -07:00
MaheshtheDev
97888ce859 feat(web): Company Brain proactiveness (automations) settings (#1207)
Adds the **Proactiveness** settings tab for Company Brain channel/DM automations.

- Accordion list of automations — collapsed rows with an instant enable toggle + **Run now**, expand to edit.
- Two-column editor: prompt (left) / deliver-to channel or DM, frequency, day, time (right), with local-timezone-aware cron.
- Preset gallery (connection-first, category-diverse) for empty state + a New-automation menu.
- DM delivery option with a tooltip explaining channel visibility + personal-connection fallback.
- Profile-menu entry (gated on Company Brain).

Pairs with the API automations work: **supermemoryai/mono#2480**.
2026-07-07 21:20:50 +00:00
MaheshtheDev
af61880da1 feat(web): add staff custom MCP connection cards (#1199)
---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/cd042b68-db46-431f-982c-3070096a37bd)
- Requested by: Unknown
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-07-06 06:11:09 +00:00
MaheshtheDev
d7050ed332 feat(web): company brain onboarding research UI (#1197)
- Confirm-domain step, then live research transcript + action rail
- Poll research status; client-side force-start fallback

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/4f4c1321-9b65-4dac-9906-8b78c4c32926)
- Requested by: Unknown
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-07-05 00:06:02 +00:00
sohamd22
8c35c1ecad review suggestions option for inferred memories (#1138)
### TL;DR

Adds a swipeable "Review suggestions" card to the dashboard that lets users approve or decline inferred memories surfaced by Nova.

### What changed?

- Added a `ReviewMemoriesCard` component that appears in the "Suggested for you" section of the dashboard (both desktop and mobile layouts). The card is hidden when there are no pending inferred memories, so it never renders empty chrome. While the modal is open, the displayed count is frozen so the trigger button doesn't tick down or disappear mid-session. Switching spaces closes the modal automatically.
- Added a `ReviewMemoriesModal` component that presents inferred memories as a swipeable card deck. Users can approve (swipe right / ✓), decline (swipe left / ✗), or skip each memory. The modal includes:
  - Drag-to-swipe with a full-card color wash (green for keep, red for decline) and verdict pills that intensify as the swipe threshold approaches
  - Keyboard support: `→` to approve, `←` to decline, `↓` or `Space` to skip, and `Cmd/Ctrl+Z` to undo
  - An undo button that steps back one card and reverts the server-side decision, using refs to avoid stale-state bugs during rapid interactions
  - A progress dot indicator showing position in the queue alongside a numeric counter
  - A "All caught up" completion state summarising how many memories were kept
  - Reduced-motion support via `useReducedMotion`
  - The card queue is snapshotted when the modal opens so cache updates from review mutations don't reshuffle the stack mid-session
- Added a `useInferredMemories` hook to fetch the pending review queue for a given container tag, and a `useReviewInferredMemory` mutation hook that calls the review endpoint. On success it removes the reviewed entry from the cached queue directly; on undo it invalidates the query to refetch the restored memory from the server.
- Registered two new API schema entries: `GET /container-tags/:containerTag/inferred` to fetch the pending queue and `POST /container-tags/:containerTag/inferred/:memoryId/review` to submit an approve, decline, or undo action.

### How to test?

1. Ensure there are inferred memories pending review for a container tag.
2. Open the dashboard — a "Review suggestions" card should appear in the "Suggested for you" section showing the count of pending memories.
3. Click the card to open the modal and swipe or use the buttons/keyboard to approve, decline, or skip memories.
4. Verify that approved and declined memories are removed from the queue after each decision and that the completion state appears once all cards are reviewed.
5. Use the undo button or `Cmd/Ctrl+Z` to step back through decisions and confirm the server-side state is reverted correctly.
6. Confirm the card does not render when there are zero pending inferred memories.
7. Switch spaces while the modal is open and confirm it closes without carrying state into the new space.

### Why make this change?

Nova infers memories on behalf of users but may not always be fully confident in them. This feature gives users a lightweight, low-friction way to review and curate those suggestions directly from the dashboard, improving the quality and trustworthiness of their memory store.
2026-07-04 20:57:33 +00:00
MaheshtheDev
acd2fea9a9 feat(web): hide getting-started once setup done, swap setup tile (#1190)
Hide the Getting Started checklist when all 3 onboarding steps complete, letting Recent memories go full-width. Swap the Setup 3/3 stat tile to a live Last updated timestamp once setup is done.

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/afbb6d10-e42e-4b40-aac9-90aba9532ec7)
- Requested by: Unknown
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-07-03 04:12:38 +00:00
MaheshtheDev
d4c451c3ce feat(web): brain connections board + MCP connect from web (#1181)
Connections board on brain home: featured app tiles with OAuth/static connect against brain/mcp-connections, agent preview prompts, connector icon set, and reworked company-brain connections in settings.

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/e37671c7-3893-48d6-ad66-73200826c04d)
- Requested by: Unknown
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-07-02 16:24:50 +00:00
MaheshtheDev
42f3aec885 feat(web): company brain onboarding redesign + connector entitlement (#1178)
- onboarding: unified About step, mode-aware Sources, Slack-focused Flows step, connect feedback (toast + connected state), auto-draft company description from domain
- brain-home: Active members stat + invite, real OneDrive icon
- useConnectorAccess hook so company_brain unlocks pro-tier connectors across onboarding + integrations
- fix company-brain-connections crash on empty connections

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/64a15b29-0848-42d1-af4c-138c5a27136f)
- Requested by: Unknown
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-06-30 22:01:20 +00:00
Dhravya Shah
e706a13877
Merge timeout additions for supermemory-mcp (#1144)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Polylane Automation <automation@nominal.ai>
2026-06-29 12:28:14 -07:00
sohamd22
1af9dbc448 add semantically mass-forget memories endpoint to docs (#1170)
## Docs: add Forget Matching endpoint + fix stale Forget Memory docs

### What this does

- **Adds docs for the new** **`POST /v4/memories/forget-matching`** **endpoint** — semantic/promptable mass-forget. Covers `dryRun` (preview), `threshold`/`maxForget` safety bounds, the request/response shape, and `forgetBatchId`.
- **Corrects the existing "Forget Memory" section** to match the actual implementation.

### ⚠️ No API surface changed

This PR is **docs-only**. The existing forget endpoint's behavior/contract is untouched — the previous docs were simply **wrong** and described a route that has never existed:

|  | Old docs (incorrect) | Actual implementation (unchanged) |
| --- | --- | --- |
| Method + path | `POST /v4/memories/{id}/forget` | `DELETE /v4/memories` |
| Body | — | `{ id \| content, containerTag, reason? }` |

The handler (`forgetMemory` in `apps/api/src/routes/v4/memories/handlers.ts`) was not modified — this just makes the docs reflect reality.

### Also

- Small accuracy cleanups (response field descriptions, realistic example IDs).
2026-06-29 18:46:18 +00:00
sohamd22
7d1428e797 docs update for profile buckets and inferred memory review (#1158)
### TL;DR

Adds documentation for the Memory Review endpoints and Profile Buckets feature.

### What changed?

**Memory Review (`memory-review.mdx`)**
- Added a new documentation page covering the two inferred memory review endpoints: `GET /v3/container-tags/{containerTag}/inferred` and `POST /v3/container-tags/{containerTag}/inferred/{memoryId}/review`.
- Documents the three review actions (`approve`, `decline`, `undo`) and how each affects search ranking and memory state (`isInference`, `isForgotten`, `reviewStatus`).
- Includes request/response examples in both `fetch` and cURL, a field reference table, error codes, and a collapsible React Query hooks example for building a review UI.
- Registered the new page in `docs.json` under the "Manage Content" group and linked to it from the Memory Operations next steps.

**Profile Buckets (`user-profiles.mdx`)**
- Added a "Profile Buckets" section explaining custom topical categories (`preferences`, `goals`, `work`, etc.) as a complement to `static`/`dynamic` profile sections.
- Documents the `include`, `buckets`, and `filters` query parameters on the profile endpoint.
- Covers the `GET /v4/profile/buckets` endpoint for listing configured bucket definitions, with request/response examples and a field reference.
- Explains the `[Recent]` / `[Summary]` label convention used in bucket and dynamic profile entries.
- Updated the `ProfileResponse` TypeScript interface to mark `static` and `dynamic` as optional and add the `buckets` field.

### How to test?

- Navigate to the docs site and confirm "Memory Review" appears in the sidebar under "Manage Content".
- Verify all code examples render correctly and tabs switch between `fetch` and cURL variants.
- Confirm the React Query accordion expands and displays the TypeScript snippet.
- Check that the Profile Buckets section renders inline within the User Profiles page, including the response JSON blocks and the tip/note callouts.

### Why make this change?

Inferred (derived) graph memories are down-weighted in search until reviewed, but there was no documentation explaining how to surface or act on them. Similarly, profile buckets were a shipped feature with no public-facing docs. These additions give developers the reference material needed to build review UIs and use topical bucket filtering in their integrations.
2026-06-29 17:23:21 +00:00
ishaanxgupta
603d0512fd Show MCP connection status in integrations (#1135)
## Summary
- Parse enabled MCP OAuth API keys from the integrations page key list.
- Show MCP as connected in MCP integration cards, the Active filter count, featured CTA, and active connections rail.
- Reuse the existing MCP metadata signal used elsewhere in the app (`sm_source: "mcp"` or `sm_kind: "mcp_oauth_exchange"`).

<img width="1918" height="657" alt="image" src="https://github.com/user-attachments/assets/0dc4dace-9d47-4f05-8aad-55bd947dfe1c" />
2026-06-28 19:33:51 +00:00
Anirudh Sharma
5d975eb0c9
Fix social URL host matching (#1174)
Co-authored-by: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com>
2026-06-28 12:36:26 +05:30
Parthiv
f1e2d3b41b
Show the orbit animation on the login panel (#1168)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 10:55:37 -07:00
ved015
3a9310778a Fix settings organization flows (#1159)
## Summary
- Fix delete-organization dialog focus when switching orgs inside Settings.
- Restyle delete-organization modal to match the app modal theme and remove extra organization icons.
- Make the organization switcher list scrollable when many orgs exist.
- Send Create organization directly to onboarding instead of opening the create-org modal.
- Stop onboarding from completing when org creation fails, show an error toast, and return existing users to the dashboard.
2026-06-27 14:27:36 +00:00
Dhravya Shah
f1ff7beb0f
Draft: Add chat source annotations (#1165) 2026-06-26 20:51:40 -07:00
Mahesh Sanikommu
d169dc078e
Fix brain home overflow (#1166) 2026-06-26 16:48:35 -07:00
Dhravya Shah
886ee692f9
Add request timeouts to Supermemory MCP client calls (#1146)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-25 13:35:23 -07:00
ishaanxgupta
4ce13cd4cf Add plugin changelog page (#1160)
## Summary
- Add a dedicated plugin changelog page under the Changelog tab, with recent updates tagged by plugin.
2026-06-25 17:06:59 +00:00
sreedharsreeram
4675904159 Fix Claude Code plugin install command (#1161)
## Summary
- update the Claude Code plugin install step to use `/plugin install supermemory`
- keep the marketplace add command pointing at `supermemoryai/claude-supermemory`

## Testing
- Not run; copy-only change
2026-06-24 23:02:43 +00:00
MaheshtheDev
3769ffa41f feat(web): Company Brain connections disconnect and Slack reconnect (#1157)
## Summary
- Add disconnect actions for org and personal GitHub/Linear connections on the Company Brain settings page
- Show connected Slack workspace name and an admin-only **Reconnect Slack** button (pairs with API admin gate in supermemoryai/mono#1912)
- Fix infinite loading skeleton when `/brain/connections` fails by falling back to empty state with a toast

## Test plan
- [ ] Open Settings → Company Brain connections as org admin
- [ ] Confirm Slack team name shows when workspace is connected
- [ ] Confirm **Reconnect Slack** is visible for admin/owner only
- [ ] Connect and disconnect GitHub/Linear for org (admin) and personal scopes
- [ ] Simulate failed connections fetch (e.g. offline) and confirm page renders instead of infinite skeleton
2026-06-24 01:16:16 +00:00
MaheshtheDev
1e1b0b1a37 feat(web): make /integrations a real route with connect deeplinks (#1155)
- Promote integrations from ?view=integrations to real /integrations and nested /integrations/[card] routes; the page body is shared via AppExperience and useViewMode is path-aware.
- Legacy ?view= URLs (and /settings/integrations) redirect to the new routes for back-compat; middleware/ensure-workspace allow the public routes.
- Add ?connect=<plugin|provider> deeplink that opens a card's connect modal instantly with a loading state (e.g. Hermes API key).
2026-06-23 20:42:29 +00:00
Oluwabusayo Jacobs
b3017eb121
chore(web): remove unused @lobbyside/react integration (#1156)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 10:00:59 -07:00
Mahesh Sanikommu
32a6055f7c
Fix integrations page double-refresh on load (#1134)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
2026-06-23 20:01:40 +05:30
MaheshtheDev
f28e974609 feat(web): add Slack connect card to home + space selector polish (#1137)
Adds the 'Add Supermemory to your Slack' card (status + install) on the home dashboard, and removes the border on the space-selector trigger.
2026-06-22 18:20:09 +00:00
MaheshtheDev
a6f7f346e2 feat(web): company brain — invite-accept page, space UX/visibility, org-scoped daily brief (#1112)
New /org/invite/[invitationId] page so consumer-org invitees can view and accept or decline invitations in the app instead of the console.

Fixes ENG-811
2026-06-22 18:12:18 +00:00
MaheshtheDev
ab3dcd7a73 feat(web): create org from settings launches onboarding (#1136)
Settings 'create organization' now routes to /onboarding?new=1&name=... (team/personal, invites) instead of a bare authClient.create. Adds the forceCreate path with name prefill, clears new=1 after a successful create to prevent duplicate orgs, and fixes the Radix popover-to-dialog pointer-events lock.
2026-06-22 18:12:18 +00:00
MaheshtheDev
504940414c feat(web): company brain entitlement helper, hook + ?org deep-link activation (#1110)
Add hasCompanyBrain helper + useHasCompanyBrain hook reading the company_brain add-on from org metadata to gate Company Brain UI.

Fixes ENG-806
2026-06-22 17:54:13 +00:00
MaheshtheDev
cf47d73126 feat(onboarding): instrument brain onboarding analytics (#1099)
Wire PostHog funnel events (started, step viewed/completed, mode, workspace, sources, ingest, team, completed) across the brain onboarding flow, and drop stale pre-brain event defs from analytics.ts.

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/5b48bbe4-422a-4577-b6bf-fd9416a6846b)
- Requested by: Sreeram Sreedhar (sreeram@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-06-20 07:48:15 +00:00
ved015
c6b20b5b87 Fix delete organization dialog focus race (#1139)
## Summary
- Delay opening the delete organization dialog until after the Danger zone popover begins closing
- Explicitly focus the organization confirmation input when the dialog opens
- Prevent intermittent focus loss where users could not type the org name
2026-06-19 17:46:21 +00:00
sreedharsreeram
313a8df6a6 feat(web): show import status on X bookmarks integration card (#1130)
## What

The X bookmarks ("Import X bookmarks") card on the integrations page never reflected any state, even after importing tweets — unlike connectors and plugins, which show "Connected"/"Active".

After this change, once you've imported at least one tweet the card shows an **"Imported · {last import time}"** pill (mirroring the plugin "Active · {time}" style) and is included in the **Connected** filter.

Since importing X bookmarks is a one-time/occasional action rather than a live connection, it intentionally says **"Imported"** with the last-import timestamp, not "Connected".

## Why

The page's status logic (`isItemConnected` + `renderStatus`) only handled the `plugin` and `connector` item kinds. The X bookmarks card is an `import` kind, so it always rendered just "Connect". This was a regression from #979 (the integrations overhaul), which rebuilt the page around item kinds and dropped the previous "{N} tweets imported" indicator.

## How

- Add a single documents query (`@post/documents/documents`, `categories: ["tweet"]`, `limit: 1`, newest first) that yields both the org-wide tweet count (`pagination.totalItems`) and the latest tweet's `createdAt`.
- `isItemConnected` returns `true` for `import` when the count is `> 0`.
- `renderStatus` renders a new `ImportedPill` ("Imported · {relative time}") for `import` when there's ≥1 tweet.

## Testing

- `tsc` and Biome pass; no new errors introduced by this change.
- Verified the status source of truth against a local backend by seeding `type='tweet'` documents — the count/latest-timestamp the pill renders reflect them.

> Note: `tsc` reports two **pre-existing**, unrelated `granola` errors in this file (`CONNECTOR_META` + an icon record missing the `granola` provider). They exist on `main` and are not touched by this PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-18 04:43:06 +00:00
sohamd22
9017a1b5d4 add weekly digests, log, and opt out options (#1107)
### TL;DR

Adds a Weekly Digests view to the web app, allowing users to browse and read their personalized weekly memory recaps directly in the UI, along with a notification preference toggle to opt out of digest emails.

### What changed?

- Added a new `digests` view mode that renders a `DigestsView` component, accessible from the dashboard via a new weekly digest preview card that appears when a digest exists.
- `DigestsView` displays a master-detail layout: a scrollable list of past weekly digests on the left, and a full digest content pane on the right. The detail pane renders the intro (with a floating brain illustration), numbered highlights, and feature recommendations styled to mirror the email layout.
- Added thumbs-up/thumbs-down feedback controls and an optional free-text input on each digest, wired to analytics events.
- Added `useDigests` and `useDigest` hooks that fetch digest list and detail data from the API, with 5- and 10-minute stale times respectively.
- Registered four new API schema endpoints: `GET /digests`, `GET /digests/:id`, `GET /digests/preferences`, and `POST /digests/preferences`.
- Added a `DigestPreferences` section to the account settings page with a toggle that lets users opt in or out of the weekly digest email.
- Extended the `viewModeChanged` analytics event and the `viewLiterals` search param list to include `"digests"`, and added `digestViewed`, `digestFeedback`, and `digestFeedbackDetail` analytics events.
- Added documentation for the `SUPERMEMORY_EMBEDDING_RAM_LIMIT` and `SUPERMEMORY_INGEST_CONCURRENCY` environment variables, explaining the memory-bounded ingestion queue and its live terminal status output.

### How to test?

1. Navigate to the dashboard and confirm the weekly digest preview card appears when a digest exists, and clicking it transitions to the `digests` view.
2. In the digests view, verify the list renders past digests with the most recent highlighted by a gradient border, and selecting a row loads the correct detail content.
3. Confirm the empty state renders correctly when no digests exist.
4. Use the thumbs-up/thumbs-down buttons and the detailed feedback textarea on a digest, verifying the toast confirmation appears on submission.
5. Open account settings, locate the "Notifications" section, and toggle the weekly digest switch on and off, verifying the preference persists without errors.
6. Confirm the `digests` view mode is reflected in the URL search params when active.

### Why make this change?

Users currently receive weekly digest emails but have no way to revisit past digests within the app. This change surfaces digest history directly in the UI, adds in-product feedback collection on digest quality, and gives users control over whether they receive the emails — improving discoverability, engagement, and preference management.
2026-06-18 01:06:40 +00:00
sreedharsreeram
d4a3a57a42 fix(web): remove duplicate "Connected" text on connector cards (#1131)
## Problem
On the integrations page → **Knowledge bases** section, a connected connector card (e.g. Google Drive) rendered the green **"Connected"** pill **twice** — once on the bottom-left, and again on the bottom-right next to the `+` button.

## Cause
The `ConnectionsCountPill` was rendered in two slots of the card:
- `renderStatus()` → left `statusSlot`
- `renderRight()` → right `actionSlot` (alongside the `+` add-source button)

The `renderStatus` connector case was added recently (#1065) and duplicated the pill that `renderRight` already shows.

## Fix
Removed the `connector` case from `renderStatus()` so the pill renders **only on the right**, in place of the Connect button. The logic is keyed on `kind === "connector"`, so this fixes every Knowledge-bases card uniformly — **Google Drive, Notion, OneDrive, Granola**.

- Non-connected cards still show the **Connect** button (unchanged).
- The info modal is unaffected — it renders `infoActionSlot ?? actionSlot` and never `statusSlot`.

```diff
-			case "connector": {
-				const count = connectionsByProvider[item.provider].length
-				if (count <= 0) return null
-				return <ConnectionsCountPill count={count} />
-			}
 			default:
 				return null
```

## Testing
Minimal, self-contained deletion (no new code, no unused symbols). Local `tsc` not run in this worktree because `node_modules` isn't installed here; the change leaves valid syntax and `ConnectionsCountPill`/`connectionsByProvider` remain used by `renderRight`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-17 18:07:25 +00:00
sreedharsreeram
00d481e8c0 Move Granola connector to Pro plan (#1132)
## Summary
- move Granola from Max-gated to Pro-gated in Nova integrations and add-connection flows
- add Granola to Pro plan card connector copy in billing
- update Granola connector docs to say Pro Plan or higher

## Testing
- bunx biome check apps/web/components/settings/billing.tsx apps/web/components/integrations-view.tsx apps/web/components/add-document/connections.tsx apps/docs/connectors/granola.mdx
- git diff --check

Note: onboarding-brain was intentionally left unchanged.
2026-06-17 17:48:04 +00:00
MaheshtheDev
def847af5f Revert "fix(mcp): clarify memory graph pagination (#1032)" (#1133)
Some checks failed
Publish OpenAI SDK Python / publish (push) Has been cancelled
This reverts commit feca81f69e.
2026-06-17 17:16:22 +00:00
icyzh
f833843760
fix(openai-sdk): use AsyncSupermemory and correct client API calls (#1063) 2026-06-17 09:05:11 -07:00
Rin
f7e97e0233
fix(web): strengthen URL validation and redirect handling in OG scraper (#1059) 2026-06-17 20:28:24 +05:30
Rāna(Bass Ver.)
feca81f69e
fix(mcp): clarify memory graph pagination (#1032)
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
2026-06-17 18:38:19 +05:30
nyxst4ck
6a4e0b2514
docs: quote pip extras install examples (#1039)
Co-authored-by: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com>
2026-06-16 23:30:41 -07:00
ishaanxgupta
24eb5a4367 Fix Granola Max upgrade gating (#1095)
## Summary

- Keep the Granola integration card in Upgrade state unless the active plan includes Max or above.
- Prevent the Granola API-key modal from opening unless the active plan is Max or above in both the integrations grid and add-document connections UI.
2026-06-17 06:22:41 +00:00
vorflux
0d2c6c8623 docs: rename Claude Code plugin to supermemory (#1100)
## Summary
- Rename the Claude Code plugin docs references from `claude-supermemory` / `Claude-Supermemory` to `supermemory`
- Update install and command examples to use `/plugin install supermemory` and `/supermemory:logout`

## Testing
- Ran `git diff --check`

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/cc17c7ae-0fb2-4e2c-bb4c-20a66900d720)
- Requested by: Sreeram Sreedhar (sreeram@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-06-17 06:14:29 +00:00
@aaronjmars
377bc979d5
fix(deps): bump next to 16.0.7 in memory-graph-playground (critical CVE) (#1082)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 21:10:32 -07:00
ved015
da70ccc5f1 feat: add chat memory imports to extension (#1115)
demo shared on work-done
2026-06-17 04:08:44 +00:00
ishaanxgupta
9c5a5d4991 Improve mobile integrations layout (#1097)
## Summary
- Add a mobile-only Activity panel with Active and Recent tabs near the top of the integrations page.
- Switch mobile integration cards from horizontal carousel cards to compact vertical list rows.
- Keep the existing desktop right-column layout unchanged.

<img width="366" height="800" alt="image" src="https://github.com/user-attachments/assets/04dc04a0-ae7b-4fe5-b54f-5d730220a90c" />
2026-06-17 04:06:59 +00:00
Mahesh Sanikommu
3f5af42565
Add console logs for debugging consent page (#1129) 2026-06-16 14:46:07 -07:00
Prasanna721
949413d98e fix mcp consent org restore (#1126)
Stops Nova's saved active org from overriding the org picked on the MCP OAuth consent page.

- skips localStorage org restoration on `/oauth/consent`
- keeps normal Nova org restore behavior everywhere else

Testing:
- `bunx biome check --write packages/lib/auth-context.tsx`
- `bun run --filter @repo/web lint` passes with existing warnings
- `bunx tsc --noEmit --project apps/web/tsconfig.json` fails on existing unrelated type errors outside this change
2026-06-16 21:19:06 +00:00
Prasanna721
e75349d82c fail closed on mcp approve (#1123)
The consent screen now fails the approve action if the MCP scope bind fails, if the OAuth consent response omits a redirect URL, or if approve tries to send the user back into a signed Better Auth interaction URL.

That prevents the UI from showing Access authorized while Claude never receives the callback.

Testing:
- bunx biome check --write apps/web/app/oauth/consent/page.tsx
- bun run --filter @repo/web lint (passes with existing warnings outside this file)
- bunx tsc --noEmit --project apps/web/tsconfig.json (fails on existing unrelated app errors)
2026-06-16 19:42:59 +00:00
Prasanna721
8de01a49d3 fix oauth resume redirect (#1122)
Fix the Claude Desktop MCP OAuth resume path after login/org selection.

- drop stale signed Better Auth params before resuming \`/oauth2/authorize\`
- consume \`prompt=login\` so approve can continue to consent/code redirect instead of looping back to login

Tested with \`bunx biome check apps/web/app/'(auth)'/login/page.tsx\`.
2026-06-16 18:31:35 +00:00
Prasanna721
651e304735 feat(web): MCP OAuth consent page (#1118)
Consent + connect UI for the new OAuth 2.1 provider. The API side lives in mono#1812 (stacked on the Enterprise MCP PR). When an MCP client starts OAuth, this is the page where you pick the org and approve access.

What's here:

- `/oauth/consent`: the consent screen. Pick an organization (cards), then set access: permission (read / read+write) and scope (full, or scoped to specific container-tag spaces with a searchable picker). Approving hands the code back to the client.
- `/connect`: plugin-aware entry for known clients (Claude Code, etc.).
- `ConsentCard.tsx`: shared card component (org list with fade, dual-icon connecting header, scoped-spaces picker), built to reuse across plugins.
- plus a fix to the mcp resource metadata.

Pairs with mono#1812 (the API OAuth provider) and the Enterprise MCP PR. Draft until the end-to-end flow is verified.
2026-06-16 17:33:09 +00:00
Ishaan Gupta
925be7c6c5
Add Antigravity MCP setup (#1119) 2026-06-16 12:10:36 +05:30
Mahesh Sanikommu
5eed4bcbf3
Add bulk multi-URL paste to create separate memories (#1109)
Some checks failed
Publish Memory Graph / publish (push) Has been cancelled
2026-06-15 11:37:09 +05:30
Mahesh Sanikommu
3b1fac2d6c
feat(web): delete organization from settings danger zone (#1117) 2026-06-15 11:36:00 +05:30
Ishaan Gupta
42b1920ce0
fix memory render (#1087) 2026-06-14 23:45:53 +05:30
Dhravya Shah
39ef7e1e5e fix: thread issue 2026-06-12 17:41:36 -07:00
sohamd22
dfff4f0fe2 add images for weekly digest email (#1106)
### TL;DR

Added new digest feature images and wordmark asset to the web app's public directory.

### What changed?

Five new images were added under `apps/web/public/images/digest/`:
- `feat-memory.png`
- `feat-profiles.png`
- `feat-retrieval.png`
- `feat-router.png`
- `wordmark.png`

### How to test?

Verify the images are accessible via their public URLs (e.g., `/images/digest/feat-memory.png`) and render correctly wherever they are referenced in the application.

### Why make this change?

These assets are needed to support the digest feature showcase, providing visual representations of the memory, profiles, retrieval, and router features, along with a wordmark for branding purposes.
2026-06-12 21:58:01 +00:00
sreedharsreeram
0ae552037a feat(mcp): playground-style rich recall (formatMemories + profile) (#1101)
## What

Makes the MCP `recall` tool render memories the same way the console **playground** does — rich, scored, relation-aware output — instead of the old flat `### Memory N (NN% match)` list.

The playground pattern is: **profile + rich search → one shared `formatMemories` formatter → feed to the model.** This ports that into the standalone MCP server.

## Changes

- **`format.ts`** (new) — `formatMemories`, lifted verbatim from the playground. Pure, dependency-free. Renders similarity scores, `agg`/`chunk` markers, related-memory arrows (`←` parent, `→` child, `~` related), attached document summaries, and temporal context.
- **`client.ts`** — `SupermemoryClient.search()` now accepts the playground's retrieval knobs (`searchMode` / `rerank` / `rewriteQuery` / `include`) and **stops discarding** the rich response fields (`context`, `documents`, `metadata`, `isAggregated`) during normalization, so the formatter has data to render.
- **`server.ts` (`handleRecall`)** — retrieves memories via `client.search()` with `include: { documents, relatedMemories }`, prepends the user's profile (stable + recent facts) when `includeProfile` is set, and renders memories through `formatMemories`. Output capped at `MAX_RECALL_CHARS`.

## How it maps to the playground

| | Playground | MCP (this PR) |
|---|---|---|
| Profile | injected into its own system prompt (`<user_profile>`) | prepended to `recall` output / exposed via `context` prompt |
| Search | hook pre-fetch **or** tool | `recall` tool (model-triggered) |
| Formatting | `formatMemories` → system prompt | `formatMemories` → `recall` tool result |

## Testing

Verified end-to-end against a local API + local MCP, driven through a real MCP client (and Claude Desktop). `recall` now returns scored memory blocks with `Source:`/`Document:` lines and the legend header. Existing e2e assertions remain compatible (they match `## User Profile` / `## Relevant Memories`, both still emitted).

## Notes

- One deliberate gap vs the playground: `aggregate` — the public SDK's `search.memories` doesn't expose it, so no `agg` synthesis blocks. Relation arrows render only when memories actually have linked parent/child versions.
- Possible follow-up: expose the `include` / `rerank` knobs as `recall` tool params so callers can toggle relations/documents per call.
- Pre-existing `tsc` errors in `server.ts` (dual `@modelcontextprotocol/sdk` install) and `mcp-app.ts` (DOM `lib`) are unrelated to this change — confirmed by an unchanged baseline.

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

---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/c616fc88-a725-4961-9011-712bce740601)
- Requested by: Sreeram Sreedhar (sreeram@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
2026-06-12 21:53:58 +00:00
MaheshtheDev
81ae72224f feat(spaces): per-space entity context + edit-space modal (#1078)
- "What to remember" per-space context — set on create, on edit, and in the space profile; steers what gets extracted into memory
- Edit-space modal (name + context) from the space pill's hover edit, mirroring the create modal
- Manual saves respect a space's configured context instead of overwriting it with the generic default
2026-06-12 20:51:37 +00:00
MaheshtheDev
40d025e178 Fix onboarding source integrations and connector gating (#1105)
Fixes the onboarding source-integrations step: corrects connector behavior, applies plan-based gating to connectors, and polishes the integrations layout.
2026-06-12 18:05:48 +00:00
Mahesh Sanikommu
94a1e30742
docs: add Container Tags concept page (#1098) 2026-06-11 16:11:09 -07:00
MaheshtheDev
f1c98c161c feat(shortcuts): merge use-case modal with shortcut actions (#1091)
Remove intermediate use-case screen; Apple Shortcuts card now opens a single modal with use cases + Add/Search memory shortcut buttons inline. Fixes res.data.key bug (was reading res.key which is always undefined). Buttons stack on mobile, no 'I'm good' dismiss.
2026-06-11 00:33:21 +00:00
ishaanxgupta
a2a1b065a2 expand chat file drop zone (#1085)
## What changed

- Added a chat-shell-level file drag/drop target so the whole chat area, including the conversation area, shows the drop overlay and accepts files.
- Disabled the nested composer drop handler for the sidebar chat path to avoid duplicate drops or stuck overlay state.
- Kept the existing local input drop behavior available for other ChatInput usages like the home composer.
2026-06-10 18:55:57 +00:00
Ishaan Gupta
6efb4ec3c2
fix(web): harden integrations upgrade plan id (#1088) 2026-06-10 11:11:03 -07:00
Dhravya Shah
0ca48baa0c
docs(readme): add Supermemory local — self-hosted binary (#1089)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 09:48:26 -07:00
Dhravya Shah
72bcf88728
docs: self-hosting section for supermemory-server (#1083)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Supermemory code review <41898282+Supermemory code review@users.noreply.github.com>
Co-authored-by: Dhravya Shah <undefined@users.noreply.github.com>
2026-06-10 09:41:52 -07:00
Vedant Mahajan
23c43bd27d
fix(web): prevent upgrade click event as plan id (#1086) 2026-06-10 21:02:42 +05:30
Ishaan Gupta
464a2b16b1
Add Cursor & granola plugin card in Integrations page (#1070) 2026-06-10 20:23:24 +05:30
Vedant Mahajan
fec9334782
Fix integrations info modal dismissal and CTA label (#1084) 2026-06-10 20:12:03 +05:30
682 changed files with 58974 additions and 32138 deletions

View file

@ -3,10 +3,15 @@ name: CI - Type Check, Format & Lint
on: on:
pull_request: pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
quality-checks: quality-checks:
name: Quality Checks name: Quality Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

View file

@ -19,6 +19,7 @@ jobs:
github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.pull_requests[0] github.event.workflow_run.pull_requests[0]
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v5
@ -41,18 +42,22 @@ jobs:
- name: Get CI failure details - name: Get CI failure details
id: failure_details id: failure_details
uses: actions/github-script@v7 uses: actions/github-script@v7
env:
RUN_ID: ${{ github.event.workflow_run.id }}
with: with:
script: | script: |
const runId = Number(process.env.RUN_ID);
const run = await github.rest.actions.getWorkflowRun({ const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }} run_id: runId
}); });
const jobs = await github.rest.actions.listJobsForWorkflowRun({ const jobs = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }} run_id: runId
}); });
const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure'); const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure');
@ -72,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
@ -82,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

@ -4,14 +4,23 @@ on:
pull_request: pull_request:
types: [opened, synchronize, ready_for_review, reopened] types: [opened, synchronize, ready_for_review, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
claude-review: claude-review:
# Fork PRs run with a read-only token and no secrets/OIDC id-token, so the
# Claude action can never authenticate there and the job always fails. Skip
# it for forks so those PRs report a clean skipped check instead of a red X.
if: | if: |
github.event.pull_request.draft == false && github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'graphite-app[bot]' && github.actor != 'graphite-app[bot]' &&
github.actor != 'dependabot[bot]' github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
@ -29,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
@ -39,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

@ -13,11 +13,36 @@ on:
jobs: jobs:
claude: claude:
if: | if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || github.event_name == 'issue_comment' &&
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || contains(github.event.comment.body, '@claude') &&
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) (github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR')
) ||
(
github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR')
) ||
(
github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
(github.event.review.author_association == 'OWNER' ||
github.event.review.author_association == 'MEMBER' ||
github.event.review.author_association == 'COLLABORATOR')
) ||
(
github.event_name == 'issues' &&
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
(github.event.issue.author_association == 'OWNER' ||
github.event.issue.author_association == 'MEMBER' ||
github.event.issue.author_association == 'COLLABORATOR')
)
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30
permissions: permissions:
contents: read contents: read
pull-requests: read pull-requests: read
@ -42,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

@ -7,9 +7,14 @@ on:
paths: paths:
- "packages/agent-framework-python/pyproject.toml" - "packages/agent-framework-python/pyproject.toml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
permissions: permissions:
contents: read contents: read
id-token: write id-token: write

View file

@ -7,9 +7,14 @@ on:
paths: paths:
- "packages/ai-sdk/package.json" - "packages/ai-sdk/package.json"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
permissions: permissions:
contents: read contents: read
id-token: write id-token: write

View file

@ -7,9 +7,14 @@ on:
paths: paths:
- "packages/cartesia-sdk-python/pyproject.toml" - "packages/cartesia-sdk-python/pyproject.toml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
permissions: permissions:
contents: read contents: read
id-token: write id-token: write

View file

@ -7,9 +7,14 @@ on:
paths: paths:
- "packages/memory-graph/package.json" - "packages/memory-graph/package.json"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
permissions: permissions:
contents: read contents: read
id-token: write id-token: write

View file

@ -7,9 +7,14 @@ on:
paths: paths:
- "packages/openai-sdk-python/pyproject.toml" - "packages/openai-sdk-python/pyproject.toml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
permissions: permissions:
contents: read contents: read
id-token: write id-token: write

View file

@ -7,9 +7,14 @@ on:
paths: paths:
- "packages/pipecat-sdk-python/pyproject.toml" - "packages/pipecat-sdk-python/pyproject.toml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
permissions: permissions:
contents: read contents: read
id-token: write id-token: write

View file

@ -7,9 +7,14 @@ on:
paths: paths:
- "packages/tools/package.json" - "packages/tools/package.json"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
permissions: permissions:
contents: read contents: read
id-token: write id-token: write

View file

@ -36,16 +36,7 @@ Before you begin, ensure you have the following installed:
# You'll need to add your API keys and database URLs # You'll need to add your API keys and database URLs
``` ```
4. **Change proxy for local development** 4. **Start the Development Server**
Add this in your `proxy.ts`(apps/web) before retrieving the cookie (`getSessionCookie(request)`):
```ts
if (url.hostname === "localhost") {
return NextResponse.next();
}
5. **Start the Development Server**
```bash ```bash
bun run dev:local bun run dev:local

103
README.md
View file

@ -13,6 +13,7 @@
<p align="center"> <p align="center">
<a href="https://supermemory.ai/docs">Docs</a> · <a href="https://supermemory.ai/docs">Docs</a> ·
<a href="https://supermemory.ai/docs/quickstart">Quickstart</a> · <a href="https://supermemory.ai/docs/quickstart">Quickstart</a> ·
<a href="https://supermemory.ai/docs/self-hosting/overview">Self-host</a> ·
<a href="https://console.supermemory.ai">Dashboard</a> · <a href="https://console.supermemory.ai">Dashboard</a> ·
<a href="https://supermemory.link/discord">Discord</a> <a href="https://supermemory.link/discord">Discord</a>
</p> </p>
@ -27,6 +28,12 @@
<strong>English</strong> · <a href="README.zh-CN.md">简体中文</a> <strong>English</strong> · <a href="README.zh-CN.md">简体中文</a>
</p> </p>
<p align="center">
<strong>#1 on every major AI memory benchmark — <a href="https://github.com/xiaowu0162/LongMemEval">LongMemEval</a>, <a href="https://github.com/snap-research/locomo">LoCoMo</a>, and <a href="https://github.com/Salesforce/ConvoMem">ConvoMem</a>.</strong><br/>
<strong>95% Recall@15 with a 99.4% context reduction · ~50ms user profiles.</strong><br/>
<a href="https://supermemory.ai/research">Read the research →</a>
</p>
--- ---
Supermemory is the memory and context layer for AI. **#1 on [LongMemEval](https://github.com/xiaowu0162/LongMemEval), [LoCoMo](https://github.com/snap-research/locomo), and [ConvoMem](https://github.com/Salesforce/ConvoMem)** — the three major benchmarks for AI memory. Supermemory is the memory and context layer for AI. **#1 on [LongMemEval](https://github.com/xiaowu0162/LongMemEval), [LoCoMo](https://github.com/snap-research/locomo), and [ConvoMem](https://github.com/Salesforce/ConvoMem)** — the three major benchmarks for AI memory.
@ -77,6 +84,21 @@ No vector DB config. No embedding pipelines. No chunking strategies.
**[→ Jump to developer quickstart](#build-with-supermemory-api)** **[→ Jump to developer quickstart](#build-with-supermemory-api)**
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>🖥️ I want to run it myself</h3>
State-of-the-art memory, on your machine. **One binary. Zero config.** Bring any model — or run fully offline with Ollama.
```bash
curl -fsSL https://supermemory.ai/install | bash
```
**[→ Jump to Supermemory local](#supermemory-local--run-it-yourself)**
</td> </td>
</tr> </tr>
</table> </table>
@ -112,13 +134,23 @@ You can find them here:
- 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
@ -160,21 +192,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)
@ -249,7 +266,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",
@ -257,7 +274,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",
@ -291,8 +308,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 |
@ -301,16 +318,53 @@ Full API reference → [supermemory.ai/docs](https://supermemory.ai/docs)
--- ---
## Supermemory local — run it yourself
State-of-the-art memory, on your machine. One binary. Zero config.
```bash
curl -fsSL https://supermemory.ai/install | bash
# or
npx supermemory local
```
```bash
supermemory-server
```
First boot sets up the embedded Supermemory graph engine, local embeddings, and your credentials, then prints an API key. The full Memory API — documents, memories, user profiles, hybrid search — runs against `http://localhost:6767`.
```typescript
const client = new Supermemory({
apiKey: "sm_...",
baseURL: "http://localhost:6767", // that's the only change
});
```
- **Bring any model** — OpenAI, Anthropic, Gemini, Groq, or any OpenAI-compatible endpoint. An interactive wizard walks you through it on first boot.
- **Embeddings** — local `Xenova/bge-base-en-v1.5` by default (no API key); optionally OpenAI, Gemini, or Ollama. Same provider stack as cloud.
- **Fully offline if you want** — point it at Ollama (`gpt-oss:20b` works great) and nothing leaves your machine.
- **Your data, one directory** — everything lives in `./.supermemory`, easy to back up or move.
- **Same API as the platform** — prototype locally, ship on the hosted platform by changing `baseURL`.
Read the [self-hosting docs](https://supermemory.ai/docs/self-hosting/overview) — quickstart, [configuration](https://supermemory.ai/docs/self-hosting/configuration), [embeddings](https://supermemory.ai/docs/self-hosting/embeddings), and [local vs. Enterprise](https://supermemory.ai/docs/self-hosting/local-vs-enterprise).
---
## Benchmarks ## Benchmarks
Supermemory is state of the art across all major AI memory benchmarks: Supermemory is state of the art across all major AI memory benchmarks:
| Benchmark | What it measures | Result | | Benchmark | What it measures | Result |
|---|---|---| |---|---|---|
| **[LongMemEval](https://github.com/xiaowu0162/LongMemEval)** | Long-term memory across sessions with knowledge updates | **81.6% — #1** | | **[LongMemEval](https://github.com/xiaowu0162/LongMemEval)** | Long-term memory across sessions with knowledge updates | **#1** |
| **[LoCoMo](https://github.com/snap-research/locomo)** | Fact recall across extended conversations (single-hop, multi-hop, temporal, adversarial) | **#1** | | **[LoCoMo](https://github.com/snap-research/locomo)** | Fact recall across extended conversations (single-hop, multi-hop, temporal, adversarial) | **#1** |
| **[ConvoMem](https://github.com/Salesforce/ConvoMem)** | Personalization and preference learning | **#1** | | **[ConvoMem](https://github.com/Salesforce/ConvoMem)** | Personalization and preference learning | **#1** |
On LongMemEval, supermemory reaches **95% Recall@15 while adding only ~720 tokens of context — a 99.4% context reduction** (99.6% at @10, 99.8% at @5). Recall by category: Knowledge Updates 99%, Assistant recall 100%, User recall 97%, Multi-session 93%, Temporal Reasoning 91%, Preference 90%.
We also built the **Supermemory Filesystem (SMFS)**, which uses **3.0× fewer tokens on Claude** (24M vs 72M) and **1.75× fewer on Codex** across the 110-question xAFS benchmark. See the full write-ups on our [research page](https://supermemory.ai/research).
We also built **[MemoryBench](https://supermemory.ai/docs/memorybench/overview)** — an open-source framework for standardized, reproducible benchmarks of memory providers. Compare Supermemory, Mem0, Zep, and others head-to-head: We also built **[MemoryBench](https://supermemory.ai/docs/memorybench/overview)** — an open-source framework for standardized, reproducible benchmarks of memory providers. Compare Supermemory, Mem0, Zep, and others head-to-head:
```bash ```bash
@ -354,6 +408,7 @@ Your app / AI tool
- 📖 [Documentation](https://supermemory.ai/docs) - 📖 [Documentation](https://supermemory.ai/docs)
- 🚀 [Quickstart](https://supermemory.ai/docs/quickstart) - 🚀 [Quickstart](https://supermemory.ai/docs/quickstart)
- 🖥️ [Self-hosting (Supermemory local)](https://supermemory.ai/docs/self-hosting/overview)
- 🧪 [MemoryBench](https://supermemory.ai/docs/memorybench/overview) - 🧪 [MemoryBench](https://supermemory.ai/docs/memorybench/overview)
- 🔌 [Integrations](https://supermemory.ai/docs/integrations) - 🔌 [Integrations](https://supermemory.ai/docs/integrations)
- 💬 [Discord](https://supermemory.link/discord) - 💬 [Discord](https://supermemory.link/discord)

View file

@ -110,13 +110,23 @@ Supermemory 已经为 Claude Code、OpenCode、OpenClaw、Hermes 提供了开箱
- 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 +168,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 +242,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 +250,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 +284,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

@ -21,11 +21,55 @@ import type {
MemoryPayload, MemoryPayload,
} from "../utils/types" } from "../utils/types"
const PLATFORM_LABELS: Record<string, string> = {
chatgpt: "ChatGPT",
claude: "Claude",
gemini: "Gemini",
t3: "T3 Chat",
twitter: "X / Twitter",
}
function normalizePlatform(value?: string): string | undefined {
if (!value) return undefined
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "")
}
function inferPlatformFromActionSource(
actionSource: string,
): string | undefined {
const source = actionSource.toLowerCase()
if (source.includes("chatgpt")) return "chatgpt"
if (source.includes("claude")) return "claude"
if (source.includes("gemini")) return "gemini"
if (source.includes("t3")) return "t3"
if (source.includes("twitter") || source.includes("x_")) return "twitter"
return undefined
}
function inferPlatformFromUrl(url?: string): string | undefined {
if (!url) return undefined
try {
const hostname = new URL(url).hostname
if (hostname === "chatgpt.com" || hostname === "chat.openai.com") {
return "chatgpt"
}
if (hostname === "claude.ai") return "claude"
if (hostname === "gemini.google.com") return "gemini"
if (hostname === "t3.chat") return "t3"
if (hostname === "x.com" || hostname === "twitter.com") return "twitter"
} catch {
return undefined
}
}
export default defineBackground(() => { export default defineBackground(() => {
let twitterImporter: TwitterImporter | null = null let twitterImporter: TwitterImporter | null = null
browser.runtime.onInstalled.addListener(async (details) => { browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason === "install") { if (details.reason === "install" || details.reason === "update") {
await trackEvent("extension_installed", { await trackEvent("extension_installed", {
reason: details.reason, reason: details.reason,
version: browser.runtime.getManifest().version, version: browser.runtime.getManifest().version,
@ -107,11 +151,33 @@ export default defineBackground(() => {
content = data?.url || "" content = data?.url || ""
} }
const platform =
normalizePlatform(data.sourcePlatform) ||
inferPlatformFromUrl(data.url) ||
inferPlatformFromActionSource(actionSource)
const platformLabel = platform
? data.sourcePlatformLabel || PLATFORM_LABELS[platform] || platform
: undefined
const metadata: MemoryPayload["metadata"] = { const metadata: MemoryPayload["metadata"] = {
sm_source: "consumer", sm_source: "consumer",
sm_origin: "browser_extension",
sm_origin_action: actionSource,
website_url: data.url, website_url: data.url,
} }
if (platform) {
metadata.sm_origin_platform = platform
}
if (platformLabel) {
metadata.sm_origin_platform_label = platformLabel
}
if (data.sourceSurface) {
metadata.sm_origin_surface = data.sourceSurface
}
if (data.ogImage) { if (data.ogImage) {
metadata.website_og_image = data.ogImage metadata.website_og_image = data.ogImage
} }
@ -148,7 +214,17 @@ export default defineBackground(() => {
eventSource: string, eventSource: string,
): Promise<{ success: boolean; data?: unknown; error?: string }> => { ): Promise<{ success: boolean; data?: unknown; error?: string }> => {
try { try {
const responseData = await searchMemories(data) 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 { const response = responseData as {
results?: Array<{ memory?: string }> results?: Array<{ memory?: string }>
} }
@ -156,7 +232,6 @@ export default defineBackground(() => {
response.results?.forEach((result, index) => { response.results?.forEach((result, index) => {
memories.push(`${index + 1}. ${result.memory} \n`) memories.push(`${index + 1}. ${result.memory} \n`)
}) })
console.log("Memories:", memories)
await trackEvent(eventSource) await trackEvent(eventSource)
return { success: true, data: memories } return { success: true, data: memories }
} catch (error) { } catch (error) {
@ -236,12 +311,12 @@ export default defineBackground(() => {
platform: string platform: string
source: string source: string
} }
console.log("=== PROMPT CAPTURED ===")
console.log(messageData)
console.log("========================")
const memoryData: MemoryData = { const memoryData: MemoryData = {
content: messageData.prompt, content: messageData.prompt,
url: messageData.source,
sourcePlatform: messageData.platform,
sourceSurface: "prompt_capture",
} }
const result = await saveMemoryToSupermemory( const result = await saveMemoryToSupermemory(

View file

@ -13,18 +13,38 @@ import {
createChatGPTInputBarElement, createChatGPTInputBarElement,
DOMUtils, DOMUtils,
} from "../../utils/ui-components" } from "../../utils/ui-components"
import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
showMemorySuggestion,
syncAcceptedSupermemoryState,
} from "./memory-suggestion"
let chatGPTDebounceTimeout: NodeJS.Timeout | null = null let chatGPTDebounceTimeout: NodeJS.Timeout | null = null
let chatGPTRouteObserver: MutationObserver | null = null let chatGPTRouteObserver: MutationObserver | null = null
let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null
let chatGPTObserverThrottle: NodeJS.Timeout | null = null let chatGPTObserverThrottle: NodeJS.Timeout | null = null
const CHATGPT_DEBUG = false
const CHATGPT_LOG_PREFIX = "[supermemory:chatgpt]"
export function initializeChatGPT() { export function initializeChatGPT() {
debugChatGPT("initializeChatGPT called", {
host: window.location.hostname,
href: window.location.href,
})
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
debugChatGPT("not on ChatGPT domain, skipping")
return return
} }
if (document.body.hasAttribute("data-chatgpt-initialized")) { if (document.body.hasAttribute("data-chatgpt-initialized")) {
debugChatGPT("already initialized")
return return
} }
@ -39,6 +59,18 @@ export function initializeChatGPT() {
setupChatGPTRouteChangeDetection() setupChatGPTRouteChangeDetection()
document.body.setAttribute("data-chatgpt-initialized", "true") document.body.setAttribute("data-chatgpt-initialized", "true")
debugChatGPT("initialized listeners")
}
function debugChatGPT(message: string, data?: unknown) {
if (!CHATGPT_DEBUG) return
if (data === undefined) {
console.log(CHATGPT_LOG_PREFIX, message)
return
}
console.log(CHATGPT_LOG_PREFIX, message, data)
} }
function setupChatGPTRouteChangeDetection() { function setupChatGPTRouteChangeDetection() {
@ -58,7 +90,7 @@ function setupChatGPTRouteChangeDetection() {
const checkForRouteChange = () => { const checkForRouteChange = () => {
if (window.location.href !== currentUrl) { if (window.location.href !== currentUrl) {
currentUrl = window.location.href currentUrl = window.location.href
console.log("ChatGPT route changed, re-adding supermemory elements") debugChatGPT("route changed, re-adding supermemory elements", currentUrl)
setTimeout(() => { setTimeout(() => {
addSupermemoryButtonToMemoriesDialog() addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn() addSaveChatGPTElementBeforeComposerBtn()
@ -83,8 +115,10 @@ function setupChatGPTRouteChangeDetection() {
if ( if (
element.querySelector?.("#prompt-textarea") || element.querySelector?.("#prompt-textarea") ||
element.querySelector?.("button.composer-btn") || element.querySelector?.("button.composer-btn") ||
element.querySelector?.("button") ||
element.querySelector?.('[role="dialog"]') || element.querySelector?.('[role="dialog"]') ||
element.matches?.("#prompt-textarea") || element.matches?.("#prompt-textarea") ||
element.matches?.("button") ||
element.id === "prompt-textarea" element.id === "prompt-textarea"
) { ) {
shouldRecheck = true shouldRecheck = true
@ -98,6 +132,7 @@ function setupChatGPTRouteChangeDetection() {
chatGPTObserverThrottle = setTimeout(() => { chatGPTObserverThrottle = setTimeout(() => {
try { try {
chatGPTObserverThrottle = null chatGPTObserverThrottle = null
debugChatGPT("DOM changed near composer, rechecking UI")
addSupermemoryButtonToMemoriesDialog() addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn() addSaveChatGPTElementBeforeComposerBtn()
setupChatGPTAutoFetch() setupChatGPTAutoFetch()
@ -124,6 +159,8 @@ function setupChatGPTRouteChangeDetection() {
async function getRelatedMemoriesForChatGPT(actionSource: string) { async function getRelatedMemoriesForChatGPT(actionSource: string) {
try { try {
const isAutoSearch =
actionSource === POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED
const userQuery = const userQuery =
document.getElementById("prompt-textarea")?.textContent || "" document.getElementById("prompt-textarea")?.textContent || ""
@ -138,7 +175,15 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
return return
} }
if (isAutoSearch) {
const promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
showLoadingSuggestion("chatgpt", promptElement)
}
setMemoryMarkerStatus(iconElement, "searching")
} else {
updateChatGPTIconFeedback("Searching memories...", iconElement) updateChatGPTIconFeedback("Searching memories...", iconElement)
}
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout( setTimeout(
@ -159,25 +204,42 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
if (response?.success && response?.data) { if (response?.success && response?.data) {
const promptElement = document.getElementById("prompt-textarea") const promptElement = document.getElementById("prompt-textarea")
if (promptElement) { if (promptElement) {
promptElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` const memoryText = showMemorySuggestion(
console.log( "chatgpt",
"Prompt element dataset:", promptElement,
promptElement.dataset.supermemories, response.data,
)
debugChatGPT("memory suggestion rendered", {
memoryLength: memoryText.length,
})
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
) )
iconElement.dataset.memoriesData = response.data if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")
} else {
updateChatGPTIconFeedback("Included Memories", iconElement) updateChatGPTIconFeedback("Included Memories", iconElement)
}
} else { } else {
console.warn( console.warn(
"ChatGPT prompt element not found after successful memory fetch", "ChatGPT prompt element not found after successful memory fetch",
) )
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")
} else {
updateChatGPTIconFeedback("Memories found", iconElement) updateChatGPTIconFeedback("Memories found", iconElement)
} }
}
} else { } else {
console.warn("No memories found or API response invalid") console.warn("No memories found or API response invalid")
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "none")
} else {
updateChatGPTIconFeedback("No memories found", iconElement) updateChatGPTIconFeedback("No memories found", iconElement)
} }
}
} catch (error) { } catch (error) {
console.error("Error getting related memories:", error) console.error("Error getting related memories:", error)
try { try {
@ -185,8 +247,14 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) {
'[id*="sm-chatgpt-input-bar-element-before-composer"]', '[id*="sm-chatgpt-input-bar-element-before-composer"]',
)[0] as HTMLElement )[0] as HTMLElement
if (icon) { if (icon) {
if (
actionSource === POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED
) {
setMemoryMarkerStatus(icon, "error")
} else {
updateChatGPTIconFeedback("Error fetching memories", icon) updateChatGPTIconFeedback("Error fetching memories", icon)
} }
}
} catch (feedbackError) { } catch (feedbackError) {
console.error("Failed to update error feedback:", feedbackError) console.error("Failed to update error feedback:", feedbackError)
} }
@ -218,7 +286,7 @@ function addSupermemoryButtonToMemoriesDialog() {
supermemoryButton.id = "supermemory-save-button" supermemoryButton.id = "supermemory-save-button"
supermemoryButton.className = "btn relative btn-primary-outline mr-2" supermemoryButton.className = "btn relative btn-primary-outline mr-2"
const iconUrl = browser.runtime.getURL("/icon-16.png") const iconUrl = browser.runtime.getURL("/new_logo.png")
supermemoryButton.innerHTML = ` supermemoryButton.innerHTML = `
<div class="flex items-center justify-center gap-2"> <div class="flex items-center justify-center gap-2">
@ -278,11 +346,16 @@ async function saveMemoriesToSupermemory() {
action: MESSAGE_TYPES.SAVE_MEMORY, action: MESSAGE_TYPES.SAVE_MEMORY,
data: { data: {
html: combinedContent, html: combinedContent,
sourcePlatform: "chatgpt",
sourceSurface: "memories_dialog",
url: window.location.href,
}, },
actionSource: "chatgpt_memories_dialog", actionSource: "chatgpt_memories_dialog",
}) })
console.log({ response }) debugChatGPT("memory dialog saved", {
success: response.success,
})
if (response.success) { if (response.success) {
DOMUtils.showToast("success") DOMUtils.showToast("success")
@ -300,255 +373,86 @@ function updateChatGPTIconFeedback(
iconElement: HTMLElement, iconElement: HTMLElement,
resetAfter = 0, resetAfter = 0,
) { ) {
if (!iconElement.dataset.originalHtml) { const memories = iconElement.dataset.memoriesData
iconElement.dataset.originalHtml = iconElement.innerHTML const fallbackReset =
} resetAfter || (message === "Included Memories" ? 0 : 2200)
const feedbackDiv = document.createElement("div") if (message === "Included Memories" || message === "Memories found") {
feedbackDiv.style.cssText = ` setMemoryMarkerStatus(iconElement, "found")
display: flex; showMarkerPopover(iconElement, "Included Memories", memories)
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 return
} }
const parent = button.parentElement if (message.toLowerCase().includes("searching")) {
if (!parent) return setMemoryMarkerStatus(iconElement, "searching")
showMarkerPopover(iconElement, message)
const parentSiblings = parent.parentElement?.children return
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 setMemoryMarkerStatus(
iconElement,
const grandParent = parent.parentElement message.toLowerCase().includes("error") ? "error" : "none",
if (!grandParent) return
const existingIcon = grandParent.querySelector(
`#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
) )
if (existingIcon) { showMarkerPopover(iconElement, message, undefined, fallbackReset)
button.setAttribute("data-supermemory-icon-added-before", "true") }
function addSaveChatGPTElementBeforeComposerBtn() {
const promptInput = getChatGPTPromptInput()
if (!promptInput) {
debugChatGPT("prompt input not found", getChatGPTDomSnapshot())
return
}
const composer = findChatGPTComposerRoot(promptInput)
if (!composer?.querySelector) {
debugChatGPT("composer root not found", describeElement(promptInput))
return
}
const existingMarkers = Array.from(
document.querySelectorAll(
`[id*="${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer"]`,
),
)
if (existingMarkers.length > 1) {
debugChatGPT("removed duplicate markers", existingMarkers.length)
for (const marker of existingMarkers) {
marker.remove()
}
} else if (existingMarkers.length === 1) {
debugChatGPT("marker already exists")
return
}
const buttons = findChatGPTComposerButtons(promptInput, composer)
debugChatGPT("candidate ChatGPT buttons", {
input: describeElement(promptInput),
composer: describeElement(composer),
buttons: buttons.map((button) => ({
label: buttonLabel(button),
element: describeElement(button),
})),
})
const micButton = buttons.find((button) => isChatGPTMicButton(button))
const voiceButton = buttons.find((button) => isChatGPTVoiceButton(button))
const sendButton = buttons.find((button) => isChatGPTSendButton(button))
const anchorButton =
micButton || voiceButton || sendButton || buttons[buttons.length - 1]
const anchorSlot = findChatGPTButtonSlot(anchorButton, composer)
const speechContainer = composer.querySelector(
'[data-testid="composer-speech-button-container"]',
) as HTMLElement | null
const targetContainer =
anchorSlot?.parentElement ||
speechContainer?.parentElement ||
promptInput.parentElement
if (!targetContainer) {
debugChatGPT("could not find insertion target", {
anchor: anchorButton ? describeElement(anchorButton) : null,
input: describeElement(promptInput),
})
return return
} }
@ -560,14 +464,153 @@ function addSaveChatGPTElementBeforeComposerBtn() {
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` 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") if (anchorSlot?.parentElement === targetContainer) {
targetContainer.insertBefore(saveChatGPTElement, anchorSlot)
grandParent.insertBefore(saveChatGPTElement, parent) debugChatGPT("inserted marker before anchor button", {
anchorLabel: anchorButton ? buttonLabel(anchorButton) : null,
anchorSlot: describeElement(anchorSlot),
target: describeElement(targetContainer),
})
} else {
targetContainer.appendChild(saveChatGPTElement)
debugChatGPT("inserted marker into fallback target", {
target: describeElement(targetContainer),
})
}
setupChatGPTAutoFetch() setupChatGPTAutoFetch()
}
function getChatGPTPromptInput(): HTMLElement | null {
return document.querySelector(
'#prompt-textarea, [data-testid="prompt-textarea"], div[contenteditable="true"]',
) as HTMLElement | null
}
function findChatGPTComposerRoot(input: HTMLElement): HTMLElement {
const form = input.closest("form") as HTMLElement | null
if (form) return form
let current: HTMLElement | null = input
for (let depth = 0; current && depth < 8; depth += 1) {
if (current.querySelectorAll("button").length >= 2) {
return current
}
current = current.parentElement
}
return input.parentElement || document.body
}
function findChatGPTComposerButtons(
input: HTMLElement,
composer: HTMLElement,
): HTMLButtonElement[] {
const composerButtons = Array.from(composer.querySelectorAll("button"))
if (composerButtons.length > 0) {
return composerButtons
}
const inputRect = input.getBoundingClientRect()
const allButtons = Array.from(document.querySelectorAll("button"))
return allButtons.filter((button) => {
const rect = button.getBoundingClientRect()
const verticallyNear =
Math.abs(
rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2),
) < 120
const horizontallyNear =
rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260
return verticallyNear && horizontallyNear
}) })
} }
function buttonLabel(button: HTMLButtonElement): string {
return [
button.id,
button.getAttribute("aria-label"),
button.getAttribute("title"),
button.getAttribute("data-testid"),
button.getAttribute("data-test-id"),
button.textContent,
]
.filter(Boolean)
.join(" ")
}
function isChatGPTMicButton(button: HTMLButtonElement): boolean {
return /mic|microphone|dictate/i.test(buttonLabel(button))
}
function isChatGPTVoiceButton(button: HTMLButtonElement): boolean {
return /voice|audio|speech/i.test(buttonLabel(button))
}
function isChatGPTSendButton(button: HTMLButtonElement): boolean {
const label = buttonLabel(button)
return /composer-submit-button|send|submit/i.test(label)
}
function findChatGPTButtonSlot(
button: HTMLButtonElement | undefined,
composer: HTMLElement,
): HTMLElement | null {
if (!button) return null
let current: HTMLElement | null = button
while (current?.parentElement && current.parentElement !== composer) {
const parent: HTMLElement = current.parentElement
const parentStyle = window.getComputedStyle(parent)
const hasSiblingControls = parent.children.length > 1
const isRow =
parentStyle.display.includes("flex") &&
parentStyle.flexDirection !== "column"
if (hasSiblingControls && isRow) {
return current
}
current = parent
}
return current || button
}
function describeElement(element: Element | null): string | null {
if (!element) return null
const parts = [element.tagName.toLowerCase()]
if (element.id) parts.push(`#${element.id}`)
if (element.className && typeof element.className === "string") {
parts.push(
`.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`,
)
}
for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) {
const value = element.getAttribute(attr)
if (value) parts.push(`[${attr}="${value}"]`)
}
return parts.join("")
}
function getChatGPTDomSnapshot() {
return {
promptTextareas: document.querySelectorAll("#prompt-textarea").length,
contenteditables: document.querySelectorAll('[contenteditable="true"]')
.length,
textareas: document.querySelectorAll("textarea").length,
buttons: document.querySelectorAll("button").length,
composerButtons: document.querySelectorAll("button.composer-btn").length,
speechContainers: document.querySelectorAll(
'[data-testid="composer-speech-button-container"]',
).length,
}
}
async function setupChatGPTAutoFetch() { async function setupChatGPTAutoFetch() {
const autoSearch = (await autoSearchEnabled.getValue()) ?? false const autoSearch = (await autoSearchEnabled.getValue()) ?? false
@ -586,12 +629,29 @@ async function setupChatGPTAutoFetch() {
promptTextarea.setAttribute("data-supermemory-auto-fetch", "true") promptTextarea.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => { const handleInput = () => {
const content = promptTextarea.textContent?.trim() || ""
syncAcceptedSupermemoryState(promptTextarea)
if (content.length === 0) {
clearMemorySuggestion("chatgpt", promptTextarea)
document
.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)
.forEach((icon) => {
setMemoryMarkerStatus(icon as HTMLElement, "neutral")
})
}
if (chatGPTDebounceTimeout) { if (chatGPTDebounceTimeout) {
clearTimeout(chatGPTDebounceTimeout) clearTimeout(chatGPTDebounceTimeout)
} }
chatGPTDebounceTimeout = setTimeout(async () => { chatGPTDebounceTimeout = setTimeout(async () => {
const content = promptTextarea.textContent?.trim() || "" if (hasAcceptedSupermemoryContext(promptTextarea)) {
clearMemorySuggestion("chatgpt", promptTextarea)
return
}
if (content.length > 2) { if (content.length > 2) {
await getRelatedMemoriesForChatGPT( await getRelatedMemoriesForChatGPT(
@ -604,6 +664,7 @@ async function setupChatGPTAutoFetch() {
icons.forEach((icon) => { icons.forEach((icon) => {
const iconElement = icon as HTMLElement const iconElement = icon as HTMLElement
setMemoryMarkerStatus(iconElement, "neutral")
if (iconElement.dataset.originalHtml) { if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml delete iconElement.dataset.originalHtml
@ -612,7 +673,7 @@ async function setupChatGPTAutoFetch() {
}) })
if (promptTextarea.dataset.supermemories) { if (promptTextarea.dataset.supermemories) {
delete promptTextarea.dataset.supermemories clearMemorySuggestion("chatgpt", promptTextarea)
} }
} }
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
@ -631,7 +692,7 @@ function setupChatGPTPromptCapture() {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) { if (!autoCapture) {
console.log("Auto capture prompts is disabled, skipping prompt capture") debugChatGPT("auto prompt capture disabled")
return return
} }
const promptTextarea = document.getElementById("prompt-textarea") const promptTextarea = document.getElementById("prompt-textarea")
@ -641,26 +702,18 @@ function setupChatGPTPromptCapture() {
promptContent = promptTextarea.textContent || "" 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()) { if (promptTextarea && promptContent.trim()) {
console.log(`ChatGPT prompt submitted via ${source}:`, promptContent) debugChatGPT("prompt submitted", {
source,
promptLength: promptContent.length,
})
try { try {
await browser.runtime.sendMessage({ await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT, action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: { data: {
prompt: promptContent, prompt: promptContent,
platform: "chatgpt", platform: "chatgpt",
source: source, source: window.location.href,
}, },
}) })
} catch (error) { } catch (error) {
@ -682,7 +735,7 @@ function setupChatGPTPromptCapture() {
}) })
if (promptTextarea?.dataset.supermemories) { if (promptTextarea?.dataset.supermemories) {
delete promptTextarea.dataset.supermemories clearMemorySuggestion("chatgpt", promptTextarea)
} }
} }
@ -705,6 +758,18 @@ function setupChatGPTPromptCapture() {
async (event) => { async (event) => {
const target = event.target as HTMLElement const target = event.target as HTMLElement
if (
(target.id === "prompt-textarea" ||
target.closest("#prompt-textarea")) &&
acceptMemorySuggestion(
event,
"chatgpt",
document.getElementById("prompt-textarea"),
)
) {
return
}
if ( if (
target.id === "prompt-textarea" && target.id === "prompt-textarea" &&
event.key === "Enter" && event.key === "Enter" &&

View file

@ -13,22 +13,43 @@ import {
createClaudeInputBarElement, createClaudeInputBarElement,
DOMUtils, DOMUtils,
} from "../../utils/ui-components" } from "../../utils/ui-components"
import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
showMemorySuggestion,
syncAcceptedSupermemoryState,
} from "./memory-suggestion"
let claudeDebounceTimeout: NodeJS.Timeout | null = null let claudeDebounceTimeout: NodeJS.Timeout | null = null
let claudeRouteObserver: MutationObserver | null = null let claudeRouteObserver: MutationObserver | null = null
let claudeUrlCheckInterval: NodeJS.Timeout | null = null let claudeUrlCheckInterval: NodeJS.Timeout | null = null
let claudeObserverThrottle: NodeJS.Timeout | null = null let claudeObserverThrottle: NodeJS.Timeout | null = null
const CLAUDE_DEBUG = false
const CLAUDE_LOG_PREFIX = "[supermemory:claude]"
export function initializeClaude() { export function initializeClaude() {
debugClaude("initializeClaude called", {
host: window.location.hostname,
href: window.location.href,
})
if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
debugClaude("not on Claude domain, skipping")
return return
} }
if (document.body.hasAttribute("data-claude-initialized")) { if (document.body.hasAttribute("data-claude-initialized")) {
debugClaude("already initialized")
return return
} }
setTimeout(() => { setTimeout(() => {
addSupermemoryButtonToClaudeMemoryDialog()
addSupermemoryIconToClaudeInput() addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch() setupClaudeAutoFetch()
}, 2000) }, 2000)
@ -38,6 +59,18 @@ export function initializeClaude() {
setupClaudeRouteChangeDetection() setupClaudeRouteChangeDetection()
document.body.setAttribute("data-claude-initialized", "true") document.body.setAttribute("data-claude-initialized", "true")
debugClaude("initialized listeners")
}
function debugClaude(message: string, data?: unknown) {
if (!CLAUDE_DEBUG) return
if (data === undefined) {
console.log(CLAUDE_LOG_PREFIX, message)
return
}
console.log(CLAUDE_LOG_PREFIX, message, data)
} }
function setupClaudeRouteChangeDetection() { function setupClaudeRouteChangeDetection() {
@ -57,8 +90,9 @@ function setupClaudeRouteChangeDetection() {
const checkForRouteChange = () => { const checkForRouteChange = () => {
if (window.location.href !== currentUrl) { if (window.location.href !== currentUrl) {
currentUrl = window.location.href currentUrl = window.location.href
console.log("Claude route changed, re-adding supermemory icon") debugClaude("route changed, re-adding supermemory icon", currentUrl)
setTimeout(() => { setTimeout(() => {
addSupermemoryButtonToClaudeMemoryDialog()
addSupermemoryIconToClaudeInput() addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch() setupClaudeAutoFetch()
}, 1000) }, 1000)
@ -79,10 +113,15 @@ function setupClaudeRouteChangeDetection() {
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element const element = node as Element
if ( if (
element.querySelector?.('[role="dialog"]') ||
element.querySelector?.('div[contenteditable="true"]') || element.querySelector?.('div[contenteditable="true"]') ||
element.querySelector?.("textarea") || element.querySelector?.("textarea") ||
element.querySelector?.("button") ||
element.matches?.('[role="dialog"]') ||
element.matches?.('div[contenteditable="true"]') || element.matches?.('div[contenteditable="true"]') ||
element.matches?.("textarea") element.matches?.("textarea") ||
element.matches?.("button") ||
element.textContent?.includes("Manage memory")
) { ) {
shouldRecheck = true shouldRecheck = true
} }
@ -95,6 +134,8 @@ function setupClaudeRouteChangeDetection() {
claudeObserverThrottle = setTimeout(() => { claudeObserverThrottle = setTimeout(() => {
try { try {
claudeObserverThrottle = null claudeObserverThrottle = null
debugClaude("DOM changed near composer, rechecking UI")
addSupermemoryButtonToClaudeMemoryDialog()
addSupermemoryIconToClaudeInput() addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch() setupClaudeAutoFetch()
} catch (error) { } catch (error) {
@ -119,20 +160,56 @@ function setupClaudeRouteChangeDetection() {
} }
function addSupermemoryIconToClaudeInput() { function addSupermemoryIconToClaudeInput() {
const targetContainers = document.querySelectorAll( const input = getClaudePromptInput()
".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0", if (!input) {
) debugClaude("prompt input not found", getClaudeDomSnapshot())
targetContainers.forEach((container) => {
if (container.hasAttribute("data-supermemory-icon-added")) {
return return
} }
const existingIcon = container.querySelector( const composer = findComposerRoot(input)
`#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`, if (!composer?.querySelector) {
debugClaude("composer root not found", describeElement(input))
return
}
const existingMarkers = Array.from(
document.querySelectorAll(
`[id*="${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}"]`,
),
) )
if (existingIcon) { if (existingMarkers.length > 1) {
container.setAttribute("data-supermemory-icon-added", "true") debugClaude("removed duplicate markers", existingMarkers.length)
for (const marker of existingMarkers) {
marker.remove()
}
} else if (existingMarkers.length === 1) {
debugClaude("marker already exists")
return
}
const buttons = findClaudeComposerButtons(input, composer)
debugClaude("candidate Claude buttons", {
input: describeElement(input),
composer: describeElement(composer),
buttons: buttons.map((button) => ({
label: buttonLabel(button),
element: describeElement(button),
})),
})
const micButton = buttons.find((button) => isClaudeMicButton(button))
const voiceButton = buttons.find((button) => isClaudeVoiceButton(button))
const sendButton = buttons.find((button) => isClaudeSendButton(button))
const anchorButton =
micButton || voiceButton || sendButton || buttons[buttons.length - 1]
const anchorSlot = findClaudeButtonSlot(anchorButton, composer)
const targetContainer = anchorSlot?.parentElement || input.parentElement
if (!targetContainer) {
debugClaude("could not find insertion target", {
anchor: anchorButton ? describeElement(anchorButton) : null,
input: describeElement(input),
})
return return
} }
@ -144,14 +221,146 @@ function addSupermemoryIconToClaudeInput() {
supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true") if (anchorSlot?.parentElement === targetContainer) {
targetContainer.insertBefore(supermemoryIcon, anchorSlot)
container.insertBefore(supermemoryIcon, container.firstChild) debugClaude("inserted marker before anchor button", {
anchorLabel: anchorButton ? buttonLabel(anchorButton) : null,
anchorSlot: describeElement(anchorSlot),
target: describeElement(targetContainer),
}) })
return
}
targetContainer.appendChild(supermemoryIcon)
debugClaude("inserted marker into fallback target", {
target: describeElement(targetContainer),
})
}
function getClaudePromptInput(): HTMLElement | null {
return document.querySelector(
'.ProseMirror[contenteditable="true"], div[contenteditable="true"], textarea',
) as HTMLElement | null
}
function findComposerRoot(input: HTMLElement): HTMLElement {
return (
(input.closest("form") as HTMLElement | null) ||
(input.closest('[data-testid*="composer"]') as HTMLElement | null) ||
(input.closest('[class*="composer"]') as HTMLElement | null) ||
(input.closest(".relative") as HTMLElement | null) ||
input.parentElement ||
document.body
)
}
function buttonLabel(button: HTMLButtonElement): string {
return [
button.getAttribute("aria-label"),
button.getAttribute("title"),
button.getAttribute("data-testid"),
button.getAttribute("data-test-id"),
button.textContent,
]
.filter(Boolean)
.join(" ")
}
function findClaudeComposerButtons(
input: HTMLElement,
composer: HTMLElement,
): HTMLButtonElement[] {
const composerButtons = Array.from(composer.querySelectorAll("button"))
if (composerButtons.length > 0) {
return composerButtons
}
const inputRect = input.getBoundingClientRect()
const allButtons = Array.from(document.querySelectorAll("button"))
return allButtons.filter((button) => {
const rect = button.getBoundingClientRect()
const verticallyNear =
Math.abs(
rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2),
) < 120
const horizontallyNear =
rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260
return verticallyNear && horizontallyNear
})
}
function isClaudeMicButton(button: HTMLButtonElement): boolean {
return /mic|microphone|dictate/i.test(buttonLabel(button))
}
function isClaudeVoiceButton(button: HTMLButtonElement): boolean {
return /voice|audio|speech/i.test(buttonLabel(button))
}
function isClaudeSendButton(button: HTMLButtonElement): boolean {
return /send|submit/i.test(buttonLabel(button))
}
function findClaudeButtonSlot(
button: HTMLButtonElement | undefined,
composer: HTMLElement,
): HTMLElement | null {
if (!button) return null
let current: HTMLElement | null = button
while (current?.parentElement && current.parentElement !== composer) {
const parent: HTMLElement = current.parentElement
const parentStyle = window.getComputedStyle(parent)
const hasSiblingControls = parent.children.length > 1
const isRow =
parentStyle.display.includes("flex") &&
parentStyle.flexDirection !== "column"
if (hasSiblingControls && isRow) {
return current
}
current = parent
}
return current || button
}
function describeElement(element: Element | null): string | null {
if (!element) return null
const parts = [element.tagName.toLowerCase()]
if (element.id) parts.push(`#${element.id}`)
if (element.className && typeof element.className === "string") {
parts.push(
`.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`,
)
}
for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) {
const value = element.getAttribute(attr)
if (value) parts.push(`[${attr}="${value}"]`)
}
return parts.join("")
}
function getClaudeDomSnapshot() {
return {
proseMirrors: document.querySelectorAll(".ProseMirror").length,
contenteditables: document.querySelectorAll('[contenteditable="true"]')
.length,
textareas: document.querySelectorAll("textarea").length,
buttons: document.querySelectorAll("button").length,
}
} }
async function getRelatedMemoriesForClaude(actionSource: string) { async function getRelatedMemoriesForClaude(actionSource: string) {
try { try {
const isAutoSearch =
actionSource === POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED
let userQuery = "" let userQuery = ""
const supermemoryContainer = document.querySelector( const supermemoryContainer = document.querySelector(
@ -188,10 +397,12 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
} }
} }
console.log("Claude query extracted:", userQuery) debugClaude("query extracted", {
queryLength: userQuery.length,
})
if (!userQuery.trim()) { if (!userQuery.trim()) {
console.log("No query text found for Claude") debugClaude("memory search skipped because query is empty")
return return
} }
@ -204,7 +415,15 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
return return
} }
if (isAutoSearch) {
const input = getClaudePromptInput()
if (input) {
showLoadingSuggestion("claude", input)
}
setMemoryMarkerStatus(iconElement, "searching")
} else {
updateClaudeIconFeedback("Searching memories...", iconElement) updateClaudeIconFeedback("Searching memories...", iconElement)
}
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout( setTimeout(
@ -222,7 +441,9 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
timeoutPromise, timeoutPromise,
]) ])
console.log("Claude memories response:", response) debugClaude("memory search response", {
success: response?.success,
})
if (response?.success && response?.data) { if (response?.success && response?.data) {
const textareaElement = document.querySelector( const textareaElement = document.querySelector(
@ -230,25 +451,42 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
) as HTMLElement ) as HTMLElement
if (textareaElement) { if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` const memoryText = showMemorySuggestion(
console.log( "claude",
"Text element dataset:", textareaElement,
textareaElement.dataset.supermemories, response.data,
)
debugClaude("memory suggestion rendered", {
memoryLength: memoryText.length,
})
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
) )
iconElement.dataset.memoriesData = response.data if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")
} else {
updateClaudeIconFeedback("Included Memories", iconElement) updateClaudeIconFeedback("Included Memories", iconElement)
}
} else { } else {
console.warn( console.warn(
"Claude input area not found after successful memory fetch", "Claude input area not found after successful memory fetch",
) )
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")
} else {
updateClaudeIconFeedback("Memories found", iconElement) updateClaudeIconFeedback("Memories found", iconElement)
} }
}
} else { } else {
console.warn("No memories found or API response invalid for Claude") console.warn("No memories found or API response invalid for Claude")
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "none")
} else {
updateClaudeIconFeedback("No memories found", iconElement) updateClaudeIconFeedback("No memories found", iconElement)
} }
}
} catch (error) { } catch (error) {
console.error("Error getting related memories for Claude:", error) console.error("Error getting related memories for Claude:", error)
try { try {
@ -256,233 +494,232 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
'[id*="sm-claude-input-bar-element"]', '[id*="sm-claude-input-bar-element"]',
) as HTMLElement ) as HTMLElement
if (icon) { if (icon) {
if (
actionSource === POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED
) {
setMemoryMarkerStatus(icon, "error")
} else {
updateClaudeIconFeedback("Error fetching memories", icon) updateClaudeIconFeedback("Error fetching memories", icon)
} }
}
} catch (feedbackError) { } catch (feedbackError) {
console.error("Failed to update Claude error feedback:", 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",
})
debugClaude("memory dialog saved", {
success: response.success,
})
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( function updateClaudeIconFeedback(
message: string, message: string,
iconElement: HTMLElement, iconElement: HTMLElement,
resetAfter = 0, resetAfter = 0,
) { ) {
if (!iconElement.dataset.originalHtml) { const memories = iconElement.dataset.memoriesData
iconElement.dataset.originalHtml = iconElement.innerHTML const fallbackReset =
resetAfter || (message === "Included Memories" ? 0 : 2200)
if (message === "Included Memories" || message === "Memories found") {
setMemoryMarkerStatus(iconElement, "found")
showMarkerPopover(iconElement, "Included Memories", memories)
return
} }
const feedbackDiv = document.createElement("div") if (message.toLowerCase().includes("searching")) {
feedbackDiv.style.cssText = ` setMemoryMarkerStatus(iconElement, "searching")
display: flex; showMarkerPopover(iconElement, message)
align-items: center; return
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 || "") setMemoryMarkerStatus(
.split(/[,\n]/) iconElement,
.map((memory) => memory.trim()) message.toLowerCase().includes("error") ? "error" : "none",
.filter((memory) => memory.length > 0 && memory !== ",") )
currentMemories.splice(index, 1) showMarkerPopover(iconElement, message, undefined, fallbackReset)
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() { function setupClaudePromptCapture() {
@ -494,7 +731,7 @@ function setupClaudePromptCapture() {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) { if (!autoCapture) {
console.log("Auto capture prompts is disabled, skipping prompt capture") debugClaude("auto prompt capture disabled")
return return
} }
let promptContent = "" let promptContent = ""
@ -514,19 +751,11 @@ function setupClaudePromptCapture() {
} }
} }
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()) { if (promptContent.trim()) {
console.log(`Claude prompt submitted via ${source}:`, promptContent) debugClaude("prompt submitted", {
source,
promptLength: promptContent.length,
})
try { try {
await browser.runtime.sendMessage({ await browser.runtime.sendMessage({
@ -534,7 +763,7 @@ function setupClaudePromptCapture() {
data: { data: {
prompt: promptContent, prompt: promptContent,
platform: "claude", platform: "claude",
source: source, source: window.location.href,
}, },
}) })
} catch (error) { } catch (error) {
@ -556,7 +785,7 @@ function setupClaudePromptCapture() {
}) })
if (contentEditableDiv?.dataset.supermemories) { if (contentEditableDiv?.dataset.supermemories) {
delete contentEditableDiv.dataset.supermemories clearMemorySuggestion("claude", contentEditableDiv)
} }
} }
@ -564,14 +793,16 @@ function setupClaudePromptCapture() {
"click", "click",
async (event) => { async (event) => {
const target = event.target as HTMLElement const target = event.target as HTMLElement
const sendButton = if (target.closest('[data-supermemory-connected-indicator="true"]')) {
target.closest( return
"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) { const sendButton = target.closest("button")
if (
sendButton &&
buttonLabel(sendButton as HTMLButtonElement).match(/send|submit/i)
) {
await captureClaudePromptContent("button click") await captureClaudePromptContent("button click")
} }
}, },
@ -583,10 +814,18 @@ function setupClaudePromptCapture() {
async (event) => { async (event) => {
const target = event.target as HTMLElement const target = event.target as HTMLElement
const activeInput =
(target.closest('div[contenteditable="true"]') as HTMLElement | null) ||
(target.matches("textarea") ? (target as HTMLTextAreaElement) : null)
if (acceptMemorySuggestion(event, "claude", activeInput)) {
return
}
if ( if (
(target.matches('div[contenteditable="true"]') || (target.matches('div[contenteditable="true"]') ||
target.matches(".ProseMirror") || target.matches(".ProseMirror") ||
target.matches("textarea") || target.matches("textarea") ||
target.closest('div[contenteditable="true"]') ||
target.closest(".ProseMirror")) && target.closest(".ProseMirror")) &&
event.key === "Enter" && event.key === "Enter" &&
!event.shiftKey !event.shiftKey
@ -618,12 +857,27 @@ async function setupClaudeAutoFetch() {
textareaElement.setAttribute("data-supermemory-auto-fetch", "true") textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => { const handleInput = () => {
const content = textareaElement.textContent?.trim() || ""
syncAcceptedSupermemoryState(textareaElement)
if (content.length === 0) {
clearMemorySuggestion("claude", textareaElement)
document
.querySelectorAll('[id*="sm-claude-input-bar-element"]')
.forEach((icon) => {
setMemoryMarkerStatus(icon as HTMLElement, "neutral")
})
}
if (claudeDebounceTimeout) { if (claudeDebounceTimeout) {
clearTimeout(claudeDebounceTimeout) clearTimeout(claudeDebounceTimeout)
} }
claudeDebounceTimeout = setTimeout(async () => { claudeDebounceTimeout = setTimeout(async () => {
const content = textareaElement.textContent?.trim() || "" if (hasAcceptedSupermemoryContext(textareaElement)) {
clearMemorySuggestion("claude", textareaElement)
return
}
if (content.length > 2) { if (content.length > 2) {
await getRelatedMemoriesForClaude( await getRelatedMemoriesForClaude(
@ -636,6 +890,7 @@ async function setupClaudeAutoFetch() {
icons.forEach((icon) => { icons.forEach((icon) => {
const iconElement = icon as HTMLElement const iconElement = icon as HTMLElement
setMemoryMarkerStatus(iconElement, "neutral")
if (iconElement.dataset.originalHtml) { if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml delete iconElement.dataset.originalHtml
@ -644,7 +899,7 @@ async function setupClaudeAutoFetch() {
}) })
if (textareaElement.dataset.supermemories) { if (textareaElement.dataset.supermemories) {
delete textareaElement.dataset.supermemories clearMemorySuggestion("claude", textareaElement)
} }
} }
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)

View file

@ -0,0 +1,664 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
UI_CONFIG,
} from "../../utils/constants"
import {
autoCapturePromptsEnabled,
autoSearchEnabled,
} from "../../utils/storage"
import {
createGeminiInputBarElement,
DOMUtils,
} from "../../utils/ui-components"
import {
acceptMemorySuggestion,
clearMemorySuggestion,
hasAcceptedSupermemoryContext,
serializeMemoriesForDataset,
setMemoryMarkerStatus,
showLoadingSuggestion,
showMarkerPopover,
showMemorySuggestion,
syncAcceptedSupermemoryState,
} from "./memory-suggestion"
let geminiDebounceTimeout: NodeJS.Timeout | null = null
let geminiRouteObserver: MutationObserver | null = null
let geminiUrlCheckInterval: NodeJS.Timeout | null = null
let geminiObserverThrottle: NodeJS.Timeout | null = null
const GEMINI_DEBUG = false
const GEMINI_LOG_PREFIX = "[supermemory:gemini]"
type GeminiInput = HTMLElement | HTMLTextAreaElement
export function initializeGemini() {
debugGemini("initializeGemini called", {
host: window.location.hostname,
href: window.location.href,
})
if (!DOMUtils.isOnDomain(DOMAINS.GEMINI)) {
debugGemini("not on Gemini domain, skipping")
return
}
if (document.body.hasAttribute("data-gemini-initialized")) {
debugGemini("already initialized")
return
}
setTimeout(() => {
addSupermemoryIconToGeminiInput()
setupGeminiAutoFetch()
}, 2000)
setupGeminiPromptCapture()
setupGeminiRouteChangeDetection()
document.body.setAttribute("data-gemini-initialized", "true")
debugGemini("initialized listeners")
}
function debugGemini(message: string, data?: unknown) {
if (!GEMINI_DEBUG) return
if (data === undefined) {
console.log(GEMINI_LOG_PREFIX, message)
return
}
console.log(GEMINI_LOG_PREFIX, message, data)
}
function setupGeminiRouteChangeDetection() {
if (geminiRouteObserver) {
geminiRouteObserver.disconnect()
}
if (geminiUrlCheckInterval) {
clearInterval(geminiUrlCheckInterval)
}
if (geminiObserverThrottle) {
clearTimeout(geminiObserverThrottle)
geminiObserverThrottle = null
}
let currentUrl = window.location.href
const recheckGeminiUI = () => {
addSupermemoryIconToGeminiInput()
setupGeminiAutoFetch()
}
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
debugGemini("route changed, rechecking UI", currentUrl)
setTimeout(recheckGeminiUI, 1000)
}
}
geminiUrlCheckInterval = setInterval(checkForRouteChange, 2000)
geminiRouteObserver = new MutationObserver((mutations) => {
if (geminiObserverThrottle) {
return
}
const shouldRecheck = mutations.some((mutation) =>
Array.from(mutation.addedNodes).some((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) {
return false
}
const element = node as Element
return (
element.matches?.("rich-textarea, textarea, button") ||
element.matches?.('[contenteditable="true"]') ||
!!element.querySelector?.(
'rich-textarea, textarea, button, [contenteditable="true"]',
)
)
}),
)
if (shouldRecheck) {
geminiObserverThrottle = setTimeout(() => {
geminiObserverThrottle = null
debugGemini("DOM changed near Gemini composer, rechecking UI")
recheckGeminiUI()
}, 300)
}
})
try {
geminiRouteObserver.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error("Failed to set up Gemini route observer:", error)
if (geminiUrlCheckInterval) {
clearInterval(geminiUrlCheckInterval)
}
geminiUrlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
function addSupermemoryIconToGeminiInput() {
const input = getGeminiPromptInput()
if (!input) {
debugGemini("prompt input not found", getGeminiDomSnapshot())
return
}
const composer = findGeminiComposerRoot(input)
if (!composer?.querySelector) {
debugGemini("composer root not found", describeElement(input))
return
}
const existingMarkers = Array.from(
document.querySelectorAll(
`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
),
)
if (existingMarkers.length > 1) {
debugGemini("removed duplicate markers", existingMarkers.length)
for (const marker of existingMarkers) {
marker.remove()
}
} else if (existingMarkers.length === 1) {
debugGemini("marker already exists")
return
}
const buttons = findGeminiComposerButtons(input, composer)
debugGemini("candidate Gemini buttons", {
input: describeElement(input),
composer: describeElement(composer),
buttons: buttons.map((button) => ({
label: buttonLabel(button),
element: describeElement(button),
})),
})
const micButton = buttons.find((button) => isGeminiMicButton(button))
const sendButton = buttons.find((button) => isGeminiSendButton(button))
const anchorButton = micButton || sendButton || buttons[buttons.length - 1]
const anchorSlot = findGeminiButtonSlot(anchorButton, composer)
const targetContainer =
anchorSlot?.parentElement ||
(input.closest("rich-textarea") as HTMLElement | null)?.parentElement ||
input.parentElement
if (!targetContainer) {
debugGemini("could not find insertion target", {
anchor: anchorButton ? describeElement(anchorButton) : null,
input: describeElement(input),
})
return
}
const supermemoryIcon = createGeminiInputBarElement(async () => {
await getRelatedMemoriesForGemini(
POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_SEARCHED,
)
})
supermemoryIcon.id = `${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
if (anchorSlot?.parentElement === targetContainer) {
targetContainer.insertBefore(supermemoryIcon, anchorSlot)
debugGemini("inserted marker before anchor button", {
anchorLabel: anchorButton ? buttonLabel(anchorButton) : null,
anchorSlot: describeElement(anchorSlot),
target: describeElement(targetContainer),
})
return
}
targetContainer.appendChild(supermemoryIcon)
debugGemini("inserted marker into fallback target", {
target: describeElement(targetContainer),
})
}
function getGeminiPromptInput(): GeminiInput | null {
return document.querySelector(
'rich-textarea .ql-editor[contenteditable="true"], rich-textarea [contenteditable="true"], .ql-editor[contenteditable="true"], div[contenteditable="true"], textarea',
) as GeminiInput | null
}
function findGeminiComposerRoot(input: GeminiInput): HTMLElement {
const form = input.closest("form") as HTMLElement | null
if (form) return form
let current: HTMLElement | null = input
for (let depth = 0; current && depth < 8; depth += 1) {
if (current.querySelectorAll("button").length >= 2) {
return current
}
current = current.parentElement
}
return input.parentElement || document.body
}
function findGeminiComposerButtons(
input: GeminiInput,
composer: HTMLElement,
): HTMLButtonElement[] {
const composerButtons = Array.from(composer.querySelectorAll("button"))
if (composerButtons.length > 0) {
return composerButtons
}
const inputRect = input.getBoundingClientRect()
const allButtons = Array.from(document.querySelectorAll("button"))
return allButtons.filter((button) => {
const rect = button.getBoundingClientRect()
const verticallyNear =
Math.abs(
rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2),
) < 120
const horizontallyNear =
rect.left > inputRect.left - 80 && rect.left < inputRect.right + 240
return verticallyNear && horizontallyNear
})
}
function buttonLabel(button: HTMLButtonElement): string {
return [
button.getAttribute("aria-label"),
button.getAttribute("title"),
button.getAttribute("data-testid"),
button.getAttribute("data-test-id"),
button.getAttribute("jsname"),
button.textContent,
]
.filter(Boolean)
.join(" ")
}
function isGeminiMicButton(button: HTMLButtonElement): boolean {
return /mic|microphone|voice|dictate|audio/i.test(buttonLabel(button))
}
function isGeminiSendButton(button: HTMLButtonElement): boolean {
const label = buttonLabel(button)
if (/send|submit/i.test(label)) {
return true
}
return !!button.querySelector(
'mat-icon[fonticon="send"], mat-icon[data-mat-icon-name="send"], [data-icon-name="send"]',
)
}
function findGeminiButtonSlot(
button: HTMLButtonElement | undefined,
composer: HTMLElement,
): HTMLElement | null {
if (!button) return null
let current: HTMLElement | null = button
while (current?.parentElement && current.parentElement !== composer) {
const parent: HTMLElement = current.parentElement
const parentStyle = window.getComputedStyle(parent)
const hasSiblingControls = parent.children.length > 1
const isRow =
parentStyle.display.includes("flex") &&
parentStyle.flexDirection !== "column"
if (hasSiblingControls && isRow) {
return current
}
current = parent
}
return current || button
}
function describeElement(element: Element | null): string | null {
if (!element) return null
const parts = [element.tagName.toLowerCase()]
if (element.id) parts.push(`#${element.id}`)
if (element.className && typeof element.className === "string") {
parts.push(
`.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`,
)
}
for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) {
const value = element.getAttribute(attr)
if (value) parts.push(`[${attr}="${value}"]`)
}
return parts.join("")
}
function getGeminiDomSnapshot() {
return {
richTextareas: document.querySelectorAll("rich-textarea").length,
qlEditors: document.querySelectorAll(".ql-editor").length,
contenteditables: document.querySelectorAll('[contenteditable="true"]')
.length,
textareas: document.querySelectorAll("textarea").length,
buttons: document.querySelectorAll("button").length,
}
}
function getInputText(input: GeminiInput | null): string {
if (!input) return ""
if (input instanceof HTMLTextAreaElement) {
return input.value || ""
}
return input.innerText || input.textContent || ""
}
async function getRelatedMemoriesForGemini(actionSource: string) {
try {
const isAutoSearch =
actionSource === POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED
const input = getGeminiPromptInput()
const userQuery = getInputText(input).trim()
debugGemini("manual/auto memory search requested", {
actionSource,
hasInput: !!input,
queryLength: userQuery.length,
})
if (!userQuery) {
debugGemini("memory search skipped because query is empty")
return
}
const iconElement = document.querySelector(
`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
) as HTMLElement | null
if (!iconElement) {
console.warn("Gemini icon element not found, cannot update feedback")
return
}
if (input && isAutoSearch) {
showLoadingSuggestion("gemini", input)
}
setMemoryMarkerStatus(iconElement, "searching")
if (!isAutoSearch) {
updateGeminiIconFeedback("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,
}),
timeoutPromise,
])) as { success?: boolean; data?: string }
debugGemini("memory search response", response)
if (response?.success && response?.data && input) {
const memoryText = showMemorySuggestion("gemini", input, response.data)
iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
iconElement.dataset.supermemories = memoryText
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "found")
} else {
updateGeminiIconFeedback("Included Memories", iconElement)
}
return
}
if (isAutoSearch) {
setMemoryMarkerStatus(iconElement, "none")
} else {
updateGeminiIconFeedback("No memories found", iconElement, 1800)
}
} catch (error) {
console.error("Error getting related memories for Gemini:", error)
const iconElement = document.querySelector(
`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
) as HTMLElement | null
if (iconElement) {
if (
actionSource === POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED
) {
setMemoryMarkerStatus(iconElement, "error")
} else {
updateGeminiIconFeedback("Error fetching memories", iconElement, 1800)
}
}
}
}
function updateGeminiIconFeedback(
message: string,
iconElement: HTMLElement,
resetAfter = 0,
) {
const memories = iconElement.dataset.memoriesData
const fallbackReset =
resetAfter || (message === "Included Memories" ? 0 : 2200)
if (message === "Included Memories" || message === "Memories found") {
setMemoryMarkerStatus(iconElement, "found")
showMarkerPopover(iconElement, "Included Memories", memories)
return
}
if (message.toLowerCase().includes("searching")) {
setMemoryMarkerStatus(iconElement, "searching")
showMarkerPopover(iconElement, message)
return
}
setMemoryMarkerStatus(
iconElement,
message.toLowerCase().includes("error") ? "error" : "none",
)
showMarkerPopover(iconElement, message, undefined, fallbackReset)
}
function setupGeminiPromptCapture() {
if (document.body.hasAttribute("data-gemini-prompt-capture-setup")) {
return
}
document.body.setAttribute("data-gemini-prompt-capture-setup", "true")
const captureGeminiPromptContent = async (source: string) => {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
debugGemini("capture requested", { source, autoCapture })
if (!autoCapture) {
debugGemini("auto prompt capture disabled")
return
}
const input = getGeminiPromptInput()
const promptContent = getInputText(input)
debugGemini("capture input state", {
hasInput: !!input,
promptLength: promptContent.length,
hasStoredMemories: !!input?.dataset.supermemories,
})
if (promptContent.trim()) {
try {
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "gemini",
source: window.location.href,
},
})
debugGemini("capture response", response)
} catch (error) {
console.error("Error sending Gemini prompt to background:", error)
}
} else {
debugGemini("capture skipped because prompt is empty")
}
const icons = document.querySelectorAll(
`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
iconElement.querySelector("[data-supermemory-status-badge]")?.remove()
delete iconElement.dataset.supermemoryStatus
delete iconElement.dataset.memoriesData
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
}
})
if (input?.dataset.supermemories) {
clearMemorySuggestion("gemini", input)
}
}
document.addEventListener(
"click",
async (event) => {
const target = event.target as HTMLElement
if (target.closest('[data-supermemory-connected-indicator="true"]')) {
return
}
const sendButton = target.closest("button")
if (sendButton && isGeminiSendButton(sendButton as HTMLButtonElement)) {
debugGemini("send button click detected", {
label: buttonLabel(sendButton as HTMLButtonElement),
element: describeElement(sendButton),
})
await captureGeminiPromptContent("button click")
}
},
true,
)
document.addEventListener(
"keydown",
async (event) => {
const target = event.target as HTMLElement
const activeInput =
(target.closest('[contenteditable="true"]') as GeminiInput | null) ||
(target.matches("textarea") ? (target as HTMLTextAreaElement) : null)
if (acceptMemorySuggestion(event, "gemini", activeInput)) {
return
}
if (
(target.matches("textarea") ||
target.matches('[contenteditable="true"]') ||
target.closest('[contenteditable="true"]')) &&
event.key === "Enter" &&
!event.shiftKey
) {
debugGemini("Enter submit detected", {
target: describeElement(target),
})
await captureGeminiPromptContent("Enter key")
}
},
true,
)
}
async function setupGeminiAutoFetch() {
const autoSearch = (await autoSearchEnabled.getValue()) ?? false
debugGemini("setup auto fetch", { autoSearch })
if (!autoSearch) {
return
}
const input = getGeminiPromptInput()
if (!input || input.hasAttribute("data-supermemory-auto-fetch")) {
debugGemini("auto fetch skipped", {
hasInput: !!input,
alreadyAttached: input?.hasAttribute("data-supermemory-auto-fetch"),
})
return
}
input.setAttribute("data-supermemory-auto-fetch", "true")
debugGemini("auto fetch attached", describeElement(input))
const handleInput = () => {
const content = getInputText(input).trim()
syncAcceptedSupermemoryState(input)
if (content.length === 0) {
clearMemorySuggestion("gemini", input)
document
.querySelectorAll(`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`)
.forEach((icon) => {
setMemoryMarkerStatus(icon as HTMLElement, "neutral")
})
}
if (geminiDebounceTimeout) {
clearTimeout(geminiDebounceTimeout)
}
geminiDebounceTimeout = setTimeout(async () => {
if (hasAcceptedSupermemoryContext(input)) {
clearMemorySuggestion("gemini", input)
return
}
if (content.length > 2) {
await getRelatedMemoriesForGemini(
POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED,
)
} else if (content.length === 0) {
const icons = document.querySelectorAll(
`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`,
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
iconElement.querySelector("[data-supermemory-status-badge]")?.remove()
delete iconElement.dataset.supermemoryStatus
delete iconElement.dataset.memoriesData
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
}
})
if (input.dataset.supermemories) {
clearMemorySuggestion("gemini", input)
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
}
input.addEventListener("input", handleInput)
}

View file

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

View file

@ -2,6 +2,8 @@ import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
import { DOMUtils } from "../../utils/ui-components" import { DOMUtils } from "../../utils/ui-components"
import { initializeChatGPT } from "./chatgpt" import { initializeChatGPT } from "./chatgpt"
import { initializeClaude } from "./claude" import { initializeClaude } from "./claude"
import { initializeGrok } from "./grok"
import { initializeGemini } from "./gemini"
import { import {
saveMemory, saveMemory,
setupGlobalKeyboardShortcut, setupGlobalKeyboardShortcut,
@ -19,13 +21,13 @@ export default defineContentScript({
matches: ["<all_urls>"], matches: ["<all_urls>"],
main() { main() {
// Setup global event listeners // Setup global event listeners
browser.runtime.onMessage.addListener(async (message) => { browser.runtime.onMessage.addListener((message) => {
if (message.action === MESSAGE_TYPES.SHOW_TOAST) { if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
DOMUtils.showToast(message.state) DOMUtils.showToast(message.state)
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
await saveMemory() return saveMemory(message.actionSource || "content_script")
} else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) { } else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) {
await openImportModal() return openImportModal()
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
updateTwitterImportUI(message) updateTwitterImportUI(message)
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
@ -48,6 +50,12 @@ export default defineContentScript({
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
initializeClaude() initializeClaude()
} }
if (DOMUtils.isOnDomain(DOMAINS.GROK)) {
initializeGrok()
}
if (DOMUtils.isOnDomain(DOMAINS.GEMINI)) {
initializeGemini()
}
if (DOMUtils.isOnDomain(DOMAINS.T3)) { if (DOMUtils.isOnDomain(DOMAINS.T3)) {
initializeT3() initializeT3()
} }
@ -65,6 +73,8 @@ export default defineContentScript({
// Initialize platform-specific functionality // Initialize platform-specific functionality
initializeChatGPT() initializeChatGPT()
initializeClaude() initializeClaude()
initializeGrok()
initializeGemini()
initializeT3() initializeT3()
initializeTwitter() initializeTwitter()

View file

@ -0,0 +1,446 @@
type SuggestionInput = HTMLElement | HTMLTextAreaElement
const SUGGESTION_ATTR = "data-supermemory-memory-suggestion"
const SUPERMEMORY_PREFIX = "Supermemories of user (only for the reference):"
const SUPERMEMORY_BLUE = "#1A88FF"
export function buildSupermemoryText(memories: unknown): string {
const memoryText = Array.isArray(memories)
? memories.join("").trim()
: String(memories || "").trim()
return `\n\n${SUPERMEMORY_PREFIX} ${memoryText}`
}
function normalizeMemoryList(memories: unknown): string[] {
const list = Array.isArray(memories)
? memories
: memories == null
? []
: [memories]
return list
.map((memory) => (typeof memory === "string" ? memory : String(memory)))
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0)
}
export function serializeMemoriesForDataset(memories: unknown): string {
const list = normalizeMemoryList(memories)
return list.length > 0 ? JSON.stringify(list) : ""
}
export function parseMemoriesFromDataset(
raw: string | null | undefined,
): string[] {
if (!raw) return []
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return normalizeMemoryList(parsed)
} catch {
// Not JSON — fall through to the legacy delimiter split.
}
return raw
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
}
export function renumberIncludedMemories(memories: string[]): string[] {
return memories.map((memory, index) => {
const text = memory.replace(/^\d+\.\s*/, "").replace(/\s+$/, "")
return `${index + 1}. ${text} \n`
})
}
export function showMemorySuggestion(
platform: string,
input: SuggestionInput,
memories: unknown,
): string {
const suggestionText = buildSupermemoryText(memories)
input.dataset.supermemories = suggestionText
delete input.dataset.supermemoriesInjected
removeMemorySuggestion(platform)
const anchor = getSuggestionAnchor(input)
if (!anchor) return suggestionText
const previousPosition = window.getComputedStyle(anchor).position
if (previousPosition === "static") {
anchor.dataset.supermemoryPreviousPosition = "static"
anchor.style.position = "relative"
}
const suggestion = createSuggestionContainer(platform, input, anchor)
suggestion.dataset.supermemorySuggestionState = "ready"
suggestion.style.gap = "8px"
suggestion.style.alignItems = "center"
const text = document.createElement("span")
text.style.cssText = `
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
text.textContent = suggestionText.trim()
const tabKey = document.createElement("span")
tabKey.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
height: 20px;
padding: 0 8px;
border-radius: 999px;
background: ${SUPERMEMORY_BLUE};
color: #FFFFFF;
font-size: 11px;
font-weight: 700;
line-height: 1;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16) inset, 0 6px 18px rgba(26, 136, 255, 0.24);
flex-shrink: 0;
`
tabKey.textContent = "Tab"
suggestion.appendChild(text)
suggestion.appendChild(tabKey)
anchor.appendChild(suggestion)
return suggestionText
}
export function showLoadingSuggestion(
platform: string,
input: SuggestionInput,
) {
removeMemorySuggestion(platform)
const anchor = getSuggestionAnchor(input)
if (!anchor) return
const previousPosition = window.getComputedStyle(anchor).position
if (previousPosition === "static") {
anchor.dataset.supermemoryPreviousPosition = "static"
anchor.style.position = "relative"
}
ensureSuggestionAnimationStyle()
const suggestion = createSuggestionContainer(platform, input, anchor)
suggestion.dataset.supermemorySuggestionState = "loading"
suggestion.style.gap = "4px"
suggestion.setAttribute("aria-label", "supermemory searching memories")
for (let index = 0; index < 3; index += 1) {
const dot = document.createElement("span")
dot.style.cssText = `
width: 5px;
height: 5px;
border-radius: 999px;
background: ${SUPERMEMORY_BLUE};
animation: supermemorySuggestionDot 1s ease-in-out infinite;
animation-delay: ${index * 0.14}s;
`
suggestion.appendChild(dot)
}
anchor.appendChild(suggestion)
}
function createSuggestionContainer(
platform: string,
input: SuggestionInput,
anchor: HTMLElement,
): HTMLDivElement {
const suggestion = document.createElement("div")
suggestion.setAttribute(SUGGESTION_ATTR, platform)
const position = getCaretPosition(input, anchor)
const verticalOffset = platform === "gemini" ? -10 : 0
suggestion.style.cssText = `
position: absolute;
left: ${position.left + 6}px;
top: ${position.top + verticalOffset}px;
max-width: min(540px, calc(100% - ${position.left + 220}px));
display: inline-flex;
align-items: center;
height: 22px;
color: rgba(255, 255, 255, 0.34);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.35;
pointer-events: none;
z-index: 2147483646;
`
return suggestion
}
export function removeMemorySuggestion(platform: string) {
const elements = document.querySelectorAll(
`[${SUGGESTION_ATTR}="${platform}"]`,
)
for (const element of elements) {
element.remove()
}
}
export function acceptMemorySuggestion(
event: KeyboardEvent,
platform: string,
input: SuggestionInput | null,
): boolean {
if (event.key !== "Tab" || !input?.dataset.supermemories) {
return false
}
event.preventDefault()
event.stopPropagation()
const text = input.dataset.supermemories
appendTextToInput(input, text)
delete input.dataset.supermemories
input.dataset.supermemoriesInjected = "true"
removeMemorySuggestion(platform)
return true
}
export function hasAcceptedSupermemoryContext(
input: SuggestionInput | null,
): boolean {
if (!input) return false
const text =
input instanceof HTMLTextAreaElement
? input.value
: input.innerText || input.textContent || ""
return text.includes(SUPERMEMORY_PREFIX)
}
export function syncAcceptedSupermemoryState(input: SuggestionInput | null) {
if (!input?.dataset.supermemoriesInjected) return
if (!hasAcceptedSupermemoryContext(input)) {
delete input.dataset.supermemoriesInjected
}
}
export function clearMemorySuggestion(
platform: string,
input: SuggestionInput | null,
) {
removeMemorySuggestion(platform)
if (input?.dataset.supermemories) {
delete input.dataset.supermemories
}
if (input?.dataset.supermemoriesInjected) {
delete input.dataset.supermemoriesInjected
}
}
export function setMemoryMarkerStatus(
iconElement: HTMLElement | null,
status: "neutral" | "searching" | "found" | "none" | "error",
) {
if (!iconElement) return
iconElement.querySelector("[data-supermemory-status-badge]")?.remove()
if (status === "neutral" || status === "none") {
delete iconElement.dataset.supermemoryStatus
return
}
iconElement.dataset.supermemoryStatus = status
const badge = document.createElement("span")
badge.dataset.supermemoryStatusBadge = "true"
badge.style.cssText = `
position: absolute;
top: 3px;
right: 3px;
width: ${status === "searching" ? "7px" : "8px"};
height: ${status === "searching" ? "7px" : "8px"};
border-radius: 999px;
background: ${status === "found" ? "#36F3D7" : status === "searching" ? SUPERMEMORY_BLUE : status === "error" ? "#EF4444" : "rgba(255, 255, 255, 0.55)"};
border: 1px solid rgba(5, 7, 10, 0.9);
box-shadow: ${status === "found" ? "0 0 0 2px rgba(54, 243, 215, 0.18)" : "none"};
pointer-events: none;
`
iconElement.appendChild(badge)
}
export function showMarkerPopover(
iconElement: HTMLElement,
message: string,
memories?: string,
resetAfter = 0,
) {
iconElement.querySelector("[data-supermemory-marker-popover]")?.remove()
ensureSuggestionAnimationStyle()
const popover = document.createElement("div")
popover.dataset.supermemoryMarkerPopover = "true"
popover.style.cssText = `
position: absolute;
right: 0;
bottom: calc(100% + 10px);
min-width: 168px;
max-width: 280px;
padding: 10px;
border-radius: 12px;
background: rgba(10, 14, 20, 0.96);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #FAFAFA;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
line-height: 1.35;
text-align: left;
z-index: 2147483647;
pointer-events: auto;
`
const title = document.createElement("div")
title.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
font-weight: 700;
margin-bottom: ${memories ? "8px" : "0"};
`
if (message.toLowerCase().includes("searching")) {
const dots = document.createElement("span")
dots.style.cssText = "display: inline-flex; gap: 3px; align-items: center;"
for (let index = 0; index < 3; index += 1) {
const dot = document.createElement("span")
dot.style.cssText = `
width: 4px;
height: 4px;
border-radius: 999px;
background: ${SUPERMEMORY_BLUE};
animation: supermemorySuggestionDot 1s ease-in-out infinite;
animation-delay: ${index * 0.14}s;
`
dots.appendChild(dot)
}
title.appendChild(dots)
}
const titleText = document.createElement("span")
titleText.textContent =
message === "Included Memories" ? "Included memories" : message
title.appendChild(titleText)
popover.appendChild(title)
if (memories) {
const list = document.createElement("div")
list.style.cssText = `
display: flex;
flex-direction: column;
gap: 6px;
max-height: 160px;
overflow-y: auto;
color: rgba(255, 255, 255, 0.76);
`
parseMemoriesFromDataset(memories)
.slice(0, 5)
.forEach((memory) => {
const item = document.createElement("div")
item.textContent = memory
list.appendChild(item)
})
popover.appendChild(list)
}
iconElement.appendChild(popover)
if (resetAfter > 0) {
setTimeout(() => {
popover.remove()
}, resetAfter)
}
}
function ensureSuggestionAnimationStyle() {
if (document.getElementById("supermemory-suggestion-animation-style")) {
return
}
const style = document.createElement("style")
style.id = "supermemory-suggestion-animation-style"
style.textContent = `
@keyframes supermemorySuggestionDot {
0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }
40% { opacity: 1; transform: translateY(-1px); }
}
`
document.head.appendChild(style)
}
function getSuggestionAnchor(input: SuggestionInput): HTMLElement | null {
return (
(input.closest("form") as HTMLElement | null) ||
(input.closest('[role="textbox"]') as HTMLElement | null)?.parentElement ||
input.parentElement
)
}
function getCaretPosition(input: SuggestionInput, anchor: HTMLElement) {
const anchorRect = anchor.getBoundingClientRect()
if (!(input instanceof HTMLTextAreaElement)) {
const selection = window.getSelection()
if (selection?.rangeCount) {
const range = selection.getRangeAt(0).cloneRange()
if (input.contains(range.startContainer)) {
range.collapse(true)
let rect = range.getBoundingClientRect()
if (rect.width === 0 && rect.height === 0) {
const marker = document.createElement("span")
marker.textContent = "\u200b"
range.insertNode(marker)
rect = marker.getBoundingClientRect()
marker.remove()
}
if (rect.width || rect.height) {
return {
left: Math.max(18, rect.right - anchorRect.left + 4),
top: Math.max(10, rect.top - anchorRect.top),
}
}
}
}
}
const inputRect = input.getBoundingClientRect()
return {
left: Math.max(18, inputRect.left - anchorRect.left + 18),
top: Math.max(10, inputRect.top - anchorRect.top + 8),
}
}
function appendTextToInput(input: SuggestionInput, text: string) {
if (input instanceof HTMLTextAreaElement) {
input.value = `${input.value}${text}`
input.dispatchEvent(new Event("input", { bubbles: true }))
return
}
input.focus()
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(input)
range.collapse(false)
range.insertNode(document.createTextNode(text))
range.collapse(false)
selection?.removeAllRanges()
selection?.addRange(range)
input.dispatchEvent(
new InputEvent("input", { bubbles: true, inputType: "insertText" }),
)
}

View file

@ -1,9 +1,12 @@
import { MESSAGE_TYPES } from "../../utils/constants" import { MESSAGE_TYPES } from "../../utils/constants"
import { bearerToken, userData } from "../../utils/storage" import { bearerToken, userData } from "../../utils/storage"
import type { APIResponse } from "../../utils/types"
import { DOMUtils } from "../../utils/ui-components" import { DOMUtils } from "../../utils/ui-components"
import { default as TurndownService } from "turndown" import { default as TurndownService } from "turndown"
export async function saveMemory() { export async function saveMemory(
actionSource = "content_script",
): Promise<APIResponse> {
try { try {
DOMUtils.showToast("loading") DOMUtils.showToast("loading")
@ -64,21 +67,28 @@ export async function saveMemory() {
data.markdown = markdown data.markdown = markdown
} }
const response = await browser.runtime.sendMessage({ const response = (await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY, action: MESSAGE_TYPES.SAVE_MEMORY,
data, data,
actionSource: "context_menu", actionSource,
}) })) as APIResponse
console.log("Response from enxtension:", response) if (response?.success) {
if (response.success) {
DOMUtils.showToast("success") DOMUtils.showToast("success")
} else { return response
}
DOMUtils.showToast("error") DOMUtils.showToast("error")
return {
success: false,
error: response?.error || "Failed to save memory",
} }
} catch (error) { } catch (error) {
console.error("Error saving memory:", error) console.error("Error saving memory:", error)
DOMUtils.showToast("error") DOMUtils.showToast("error")
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}
} }
} }
@ -90,7 +100,7 @@ export function setupGlobalKeyboardShortcut() {
event.key === "m" event.key === "m"
) { ) {
event.preventDefault() event.preventDefault()
await saveMemory() await saveMemory("keyboard_shortcut")
} }
}) })
} }
@ -110,9 +120,6 @@ export function setupStorageListener() {
window.location.hostname === "app.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 return
} }

View file

@ -10,11 +10,30 @@ import {
autoCapturePromptsEnabled, autoCapturePromptsEnabled,
} from "../../utils/storage" } from "../../utils/storage"
import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components" import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components"
import {
buildSupermemoryText,
parseMemoriesFromDataset,
renumberIncludedMemories,
serializeMemoriesForDataset,
} from "./memory-suggestion"
let t3DebounceTimeout: NodeJS.Timeout | null = null let t3DebounceTimeout: NodeJS.Timeout | null = null
let t3RouteObserver: MutationObserver | null = null let t3RouteObserver: MutationObserver | null = null
let t3UrlCheckInterval: NodeJS.Timeout | null = null let t3UrlCheckInterval: NodeJS.Timeout | null = null
let t3ObserverThrottle: NodeJS.Timeout | null = null let t3ObserverThrottle: NodeJS.Timeout | null = null
let t3IncludedPopup: {
el: HTMLElement
onClick: (event: MouseEvent) => void
timer: ReturnType<typeof setTimeout>
} | null = null
function disposeT3IncludedPopup() {
if (!t3IncludedPopup) return
document.removeEventListener("click", t3IncludedPopup.onClick)
clearTimeout(t3IncludedPopup.timer)
t3IncludedPopup.el.remove()
t3IncludedPopup = null
}
export function initializeT3() { export function initializeT3() {
if (!DOMUtils.isOnDomain(DOMAINS.T3)) { if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
@ -26,7 +45,6 @@ export function initializeT3() {
} }
setTimeout(() => { setTimeout(() => {
console.log("Adding supermemory icon to T3 input")
addSupermemoryIconToT3Input() addSupermemoryIconToT3Input()
setupT3AutoFetch() setupT3AutoFetch()
}, 2000) }, 2000)
@ -54,8 +72,8 @@ function setupT3RouteChangeDetection() {
const checkForRouteChange = () => { const checkForRouteChange = () => {
if (window.location.href !== currentUrl) { if (window.location.href !== currentUrl) {
disposeT3IncludedPopup()
currentUrl = window.location.href currentUrl = window.location.href
console.log("T3 route changed, re-adding supermemory icon")
setTimeout(() => { setTimeout(() => {
addSupermemoryIconToT3Input() addSupermemoryIconToT3Input()
setupT3AutoFetch() setupT3AutoFetch()
@ -183,10 +201,7 @@ async function getRelatedMemoriesForT3(actionSource: string) {
} }
} }
console.log("T3 query extracted:", userQuery)
if (!userQuery.trim()) { if (!userQuery.trim()) {
console.log("No query text found for T3")
return return
} }
@ -217,8 +232,6 @@ async function getRelatedMemoriesForT3(actionSource: string) {
timeoutPromise, timeoutPromise,
]) ])
console.log("T3 memories response:", response)
if (response?.success && response?.data) { if (response?.success && response?.data) {
let textareaElement = null let textareaElement = null
const supermemoryContainer = document.querySelector( const supermemoryContainer = document.querySelector(
@ -238,9 +251,13 @@ async function getRelatedMemoriesForT3(actionSource: string) {
} }
if (textareaElement) { if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` textareaElement.dataset.supermemories = buildSupermemoryText(
response.data,
)
iconElement.dataset.memoriesData = response.data iconElement.dataset.memoriesData = serializeMemoriesForDataset(
response.data,
)
updateT3IconFeedback("Included Memories", iconElement) updateT3IconFeedback("Included Memories", iconElement)
} else { } else {
@ -275,6 +292,8 @@ function updateT3IconFeedback(
iconElement.dataset.originalHtml = iconElement.innerHTML iconElement.dataset.originalHtml = iconElement.innerHTML
} }
disposeT3IncludedPopup()
const feedbackDiv = document.createElement("div") const feedbackDiv = document.createElement("div")
feedbackDiv.style.cssText = ` feedbackDiv.style.cssText = `
display: flex; display: flex;
@ -336,13 +355,9 @@ function updateT3IconFeedback(
overflow-y: auto; overflow-y: auto;
` `
const memoriesText = iconElement.dataset.memoriesData || "" const individualMemories = parseMemoriesFromDataset(
console.log("Memories text:", memoriesText) iconElement.dataset.memoriesData,
const individualMemories = memoriesText )
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
console.log("Individual memories:", individualMemories)
individualMemories.forEach((memory, index) => { individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div") const memoryItem = document.createElement("div")
@ -414,66 +429,65 @@ function updateT3IconFeedback(
popup.style.display = "block" popup.style.display = "block"
}) })
document.addEventListener("click", (e) => { const onClick = (e: MouseEvent) => {
if (!popup.contains(e.target as Node)) { if (!popup.contains(e.target as Node)) {
popup.style.display = "none" popup.style.display = "none"
} }
}) }
document.addEventListener("click", onClick)
t3IncludedPopup = {
el: popup,
onClick,
timer: setTimeout(disposeT3IncludedPopup, 300000),
}
content.querySelectorAll("button[data-memory-index]").forEach((button) => { content.querySelectorAll("button[data-memory-index]").forEach((button) => {
const htmlButton = button as HTMLButtonElement const htmlButton = button as HTMLButtonElement
htmlButton.addEventListener("click", () => { htmlButton.addEventListener("click", () => {
const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10) const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
const memoryItem = htmlButton.parentElement htmlButton.parentElement?.remove()
if (memoryItem) { const remainingMemories = parseMemoriesFromDataset(
content.removeChild(memoryItem) iconElement.dataset.memoriesData,
} )
remainingMemories.splice(index, 1)
const currentMemories = (iconElement.dataset.memoriesData || "") const remaining = renumberIncludedMemories(remainingMemories)
.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 = const textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) || (document.querySelector("textarea") as HTMLTextAreaElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement) (document.querySelector('div[contenteditable="true"]') as HTMLElement)
// Only wipe when nothing remains — `<= 1` used to discard the last kept memory.
if (remaining.length === 0) {
if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
delete iconElement.dataset.memoriesData
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
disposeT3IncludedPopup()
return
}
iconElement.dataset.memoriesData =
serializeMemoriesForDataset(remaining)
if (textareaElement) { if (textareaElement) {
textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}` textareaElement.dataset.supermemories =
buildSupermemoryText(remaining)
} }
content content
.querySelectorAll("button[data-memory-index]") .querySelectorAll("button[data-memory-index]")
.forEach((btn, newIndex) => { .forEach((btn, newIndex) => {
const htmlBtn = btn as HTMLButtonElement const htmlBtn = btn as HTMLButtonElement
htmlBtn.dataset.memoryIndex = newIndex.toString() htmlBtn.dataset.memoryIndex = String(newIndex)
}) const label = htmlBtn.previousElementSibling
if (label) {
if (currentMemories.length <= 1) { label.textContent = remaining[newIndex].trim()
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.innerHTML = ""
@ -493,11 +507,10 @@ function setupT3PromptCapture() {
} }
document.body.setAttribute("data-t3-prompt-capture-setup", "true") document.body.setAttribute("data-t3-prompt-capture-setup", "true")
const captureT3PromptContent = async (source: string) => { const captureT3PromptContent = async (_source: string) => {
const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false
if (!autoCapture) { if (!autoCapture) {
console.log("Auto capture prompts is disabled, skipping prompt capture")
return return
} }
let promptContent = "" let promptContent = ""
@ -538,15 +551,13 @@ function setupT3PromptCapture() {
} }
if (promptContent.trim()) { if (promptContent.trim()) {
console.log(`T3 prompt submitted via ${source}:`, promptContent)
try { try {
await browser.runtime.sendMessage({ await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT, action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: { data: {
prompt: promptContent, prompt: promptContent,
platform: "t3", platform: "t3",
source: source, source: window.location.href,
}, },
}) })
} catch (error) { } catch (error) {
@ -568,6 +579,7 @@ function setupT3PromptCapture() {
if (textareaElement?.dataset.supermemories) { if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories delete textareaElement.dataset.supermemories
} }
disposeT3IncludedPopup()
} }
const handleT3SendButtonClick = async (event: Event) => { const handleT3SendButtonClick = async (event: Event) => {
@ -723,6 +735,7 @@ async function setupT3AutoFetch() {
if (textareaElement.dataset.supermemories) { if (textareaElement.dataset.supermemories) {
delete textareaElement.dataset.supermemories delete textareaElement.dataset.supermemories
} }
disposeT3IncludedPopup()
} }
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
} }

View file

@ -253,7 +253,7 @@ async function showOnboardingToast() {
header.style.cssText = header.style.cssText =
"display: flex; align-items: flex-start; gap: 12px; position: relative;" "display: flex; align-items: flex-start; gap: 12px; position: relative;"
const iconUrl = browser.runtime.getURL("/icon-16.png") const iconUrl = browser.runtime.getURL("/new_logo.png")
const icon = document.createElement("img") const icon = document.createElement("img")
icon.src = iconUrl icon.src = iconUrl
icon.alt = "Supermemory" icon.alt = "Supermemory"
@ -512,7 +512,7 @@ function showOrUpdateImportProgressToast(message: string, isComplete = false) {
animation: smSlideInUp 0.3s ease-out; animation: smSlideInUp 0.3s ease-out;
` `
const iconUrl = browser.runtime.getURL("/icon-16.png") const iconUrl = browser.runtime.getURL("/new_logo.png")
const icon = document.createElement("img") const icon = document.createElement("img")
icon.src = iconUrl icon.src = iconUrl
icon.alt = "Supermemory" icon.alt = "Supermemory"

View file

@ -2,7 +2,12 @@ import { useQueryClient } from "@tanstack/react-query"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import "./App.css" import "./App.css"
import { validateAuthToken } from "../../utils/api" import { validateAuthToken } from "../../utils/api"
import { MESSAGE_TYPES, STORAGE_KEYS, UI_CONFIG } from "../../utils/constants" import {
getSupermemoryLoginUrl,
MESSAGE_TYPES,
STORAGE_KEYS,
UI_CONFIG,
} from "../../utils/constants"
import { import {
useDefaultProject, useDefaultProject,
useProjects, useProjects,
@ -70,6 +75,167 @@ const Tooltip = ({
) )
} }
const cardShadow =
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset"
type ManualImportProvider = "gemini"
const manualImportProviderConfig: Record<
ManualImportProvider,
{ label: string; actionSource: string }
> = {
gemini: {
label: "Gemini",
actionSource: "gemini_manual_memory_import",
},
}
const manualMemoryImportPrompt = `Export all of my stored memories and any context you've learned about me from past conversations. Preserve my words verbatim where possible, especially for instructions and preferences.
## Categories (output in this order):
1. **Instructions**: Rules I've explicitly asked you to follow going forward - tone, format, style, "always do X", "never do Y", and corrections to your behavior. Only include rules from stored memories, not from conversations.
2. **Identity**: Name, age, location, education, family, relationships, languages, and personal interests.
3. **Career**: Current and past roles, companies, and general skill areas.
4. **Projects**: Projects I meaningfully built or committed to. Ideally ONE entry per project. Include what it does, current status, and any key decisions. Use the project name or a short descriptor as the first words of the entry.
5. **Preferences**: Opinions, tastes, and working-style preferences that apply broadly.
## Format:
Use section headers for each category. Within each category, list one entry per line, sorted by oldest date first. Format each line as:
[YYYY-MM-DD] - Entry content here.
If no date is known, use [unknown] instead.
## Output:
- Wrap the entire export in a single code block for easy copying.
- After the code block, state whether this is the complete set or if more remain.`
const normalizeManualMemoryImport = (value: string) => {
const trimmed = value.trim()
const codeBlockMatch = trimmed.match(/```(?:[\w-]+)?\s*([\s\S]*?)```/)
return (codeBlockMatch?.[1] ?? trimmed).trim()
}
const OpenAILogo = ({ className }: { className?: string }) => (
<svg
aria-label="ChatGPT Logo"
className={className}
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
)
const ClaudeLogo = ({ className }: { className?: string }) => (
<img alt="Claude" className={className} src="./claude.png" />
)
const GeminiLogo = ({ className }: { className?: string }) => (
<img alt="Gemini" className={className} src="./gemini.png" />
)
const XLogo = ({ className }: { className?: string }) => (
<svg
aria-label="X Twitter Logo"
className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>X Twitter Logo</title>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
)
const GrokLogo = ({ className }: { className?: string }) => (
<svg
aria-label="Grok Logo"
className={className}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Grok</title>
<path
d="M17.85 6.35A7.3 7.3 0 0 0 6.2 14.75"
stroke="white"
strokeLinecap="square"
strokeWidth="2.7"
/>
<path
d="M6.15 17.65A7.3 7.3 0 0 0 17.8 9.25"
stroke="white"
strokeLinecap="square"
strokeWidth="2.7"
/>
<path
d="M3.8 20.2L20.2 3.8"
stroke="white"
strokeLinecap="round"
strokeWidth="2.4"
/>
</svg>
)
const ChatAppsLogo = ({ className }: { className?: string }) => (
<div className={`relative h-5 w-[42px] shrink-0 ${className || ""}`}>
<div className="absolute left-0 top-0 flex h-5 w-5 items-center justify-center rounded-[7px] border border-[#FFFFFF1A] bg-[#214E54] shadow-[0_0_0_1px_rgba(0,0,0,0.35),0_4px_12px_rgba(0,0,0,0.25)]">
<OpenAILogo className="h-3 w-3 text-white" />
</div>
<div className="absolute left-[13px] top-0 flex h-5 w-5 items-center justify-center rounded-[7px] border border-[#FFFFFF1A] bg-[#2A1710] shadow-[0_0_0_1px_rgba(0,0,0,0.35),0_4px_12px_rgba(0,0,0,0.25)]">
<ClaudeLogo className="h-3 w-3" />
</div>
<div className="absolute left-[26px] top-0 flex h-5 w-5 items-center justify-center rounded-[7px] border border-[#FFFFFF1A] bg-[#111820] shadow-[0_0_0_1px_rgba(0,0,0,0.35),0_4px_12px_rgba(0,0,0,0.25)]">
<GrokLogo className="h-3 w-3" />
</div>
</div>
)
const ImportCard = ({
icon,
title,
description,
onClick,
}: {
icon: React.ReactNode
title: string
description?: string
onClick: () => void
}) => (
<button
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-between gap-3 transition-colors duration-200 hover:bg-[#5B7EF520]"
style={{
boxShadow: cardShadow,
}}
onClick={onClick}
type="button"
>
<div className="text-left min-w-0">
<p className="flex items-center gap-2 font-medium">
{icon}
{title}
</p>
{description && (
<p className="m-0 text-[14px] text-[#737373] leading-tight">
{description}
</p>
)}
</div>
<RightArrow className="size-4 shrink-0 mt-1" />
</button>
)
function App() { function App() {
const [userSignedIn, setUserSignedIn] = useState<boolean>(false) const [userSignedIn, setUserSignedIn] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(true) const [loading, setLoading] = useState<boolean>(true)
@ -80,10 +246,19 @@ function App() {
const [activeTab, setActiveTab] = useState<"save" | "imports" | "settings">( const [activeTab, setActiveTab] = useState<"save" | "imports" | "settings">(
"save", "save",
) )
const [showChatAppImports, setShowChatAppImports] = useState<boolean>(false)
const [manualImportProvider, setManualImportProvider] =
useState<ManualImportProvider | null>(null)
const [manualImportText, setManualImportText] = useState<string>("")
const [manualImportSaving, setManualImportSaving] = useState<boolean>(false)
const [manualImportSaved, setManualImportSaved] = useState<boolean>(false)
const [manualImportCopied, setManualImportCopied] = useState<boolean>(false)
const [manualImportError, setManualImportError] = useState<string>("")
const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false) const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false)
const [autoCapturePromptsEnabled, setAutoCapturePromptsEnabled] = const [autoCapturePromptsEnabled, setAutoCapturePromptsEnabled] =
useState<boolean>(false) useState<boolean>(false)
const [authInvalidated, setAuthInvalidated] = useState<boolean>(false) const [authInvalidated, setAuthInvalidated] = useState<boolean>(false)
const [saveError, setSaveError] = useState<string | null>(null)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { data: projects = [], isLoading: loadingProjects } = useProjects({ const { data: projects = [], isLoading: loadingProjects } = useProjects({
@ -196,33 +371,80 @@ function App() {
// biome-ignore lint/correctness/useExhaustiveDependencies: close space selector when tab changes // biome-ignore lint/correctness/useExhaustiveDependencies: close space selector when tab changes
useEffect(() => { useEffect(() => {
setShowProjectSelector(false) setShowProjectSelector(false)
setShowChatAppImports(false)
setManualImportProvider(null)
setManualImportText("")
setManualImportSaved(false)
setManualImportCopied(false)
setManualImportError("")
}, [activeTab]) }, [activeTab])
const handleSaveCurrentPage = async () => { const handleSaveCurrentPage = async () => {
setSaving(true) setSaving(true)
setSaveError(null)
try { try {
const tabs = await chrome.tabs.query({ const tabs = await chrome.tabs.query({
active: true, active: true,
currentWindow: true, currentWindow: true,
}) })
if (tabs.length > 0 && tabs[0].id) { const tab = tabs[0]
const response = await chrome.tabs.sendMessage(tabs[0].id, { let response: { success?: boolean; error?: string } | undefined
if (tab?.id) {
try {
response = await chrome.tabs.sendMessage(tab.id, {
action: MESSAGE_TYPES.SAVE_MEMORY, action: MESSAGE_TYPES.SAVE_MEMORY,
actionSource: "popup", actionSource: "popup",
}) })
} catch (contentScriptError) {
console.warn("Content script save failed:", contentScriptError)
}
}
if (response?.success) { if (response && !response.success) {
await chrome.tabs.sendMessage(tabs[0].id, { throw new Error(response.error || "Failed to save current page")
action: MESSAGE_TYPES.SHOW_TOAST, }
state: "success",
if (!response) {
const fallbackUrl = tab?.url || currentUrl
const fallbackTitle = tab?.title || currentTitle || "Current Page"
if (!fallbackUrl) {
throw new Error("No active page URL found")
}
response = await chrome.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
actionSource: "popup_fallback",
data: {
url: fallbackUrl,
title: fallbackTitle,
content: `${fallbackTitle}\n\n${fallbackUrl}`,
},
}) })
} }
window.close() if (response?.success) {
if (tab?.id) {
await chrome.tabs
.sendMessage(tab.id, {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "success",
})
.catch(() => undefined)
} }
window.close()
return
}
throw new Error(response?.error || "Failed to save current page")
} catch (error) { } catch (error) {
console.error("Failed to save current page:", error) console.error("Failed to save current page:", error)
setSaveError(
error instanceof Error ? error.message : "Could not save page",
)
try { try {
const tabs = await chrome.tabs.query({ const tabs = await chrome.tabs.query({
@ -238,8 +460,6 @@ function App() {
} catch (toastError) { } catch (toastError) {
console.error("Failed to show error toast:", toastError) console.error("Failed to show error toast:", toastError)
} }
window.close()
} finally { } finally {
setSaving(false) setSaving(false)
} }
@ -263,6 +483,125 @@ function App() {
} }
} }
const handleTwitterBookmarksImport = async () => {
const targetUrl = "https://x.com/i/bookmarks"
try {
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true,
})
const isOnBookmarksPage =
activeTab?.url?.includes("x.com/i/bookmarks") ||
activeTab?.url?.includes("twitter.com/i/bookmarks")
if (isOnBookmarksPage && activeTab?.id) {
try {
await chrome.tabs.sendMessage(activeTab.id, {
action: MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL,
})
} catch (error) {
console.error("Failed to send message to content script:", error)
const intentExpiry = Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
await chrome.storage.local.set({
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]: intentExpiry,
})
await chrome.tabs.create({
url: targetUrl,
})
}
} else {
const intentExpiry = Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
await chrome.storage.local.set({
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]: intentExpiry,
})
await chrome.tabs.create({
url: targetUrl,
})
}
} catch (error) {
console.error("Error opening Twitter import:", error)
try {
await chrome.tabs.create({
url: targetUrl,
})
} catch (fallbackError) {
console.error("Failed to open bookmarks page:", fallbackError)
}
}
}
const handleOpenManualMemoryImport = (provider: ManualImportProvider) => {
setManualImportProvider(provider)
setManualImportText("")
setManualImportSaved(false)
setManualImportCopied(false)
setManualImportError("")
}
const handleCloseManualMemoryImport = () => {
setManualImportProvider(null)
setManualImportText("")
setManualImportSaved(false)
setManualImportCopied(false)
setManualImportError("")
}
const handleCopyManualImportPrompt = async () => {
try {
await navigator.clipboard.writeText(manualMemoryImportPrompt)
setManualImportCopied(true)
window.setTimeout(() => setManualImportCopied(false), 1600)
} catch (error) {
console.error("Failed to copy memory import prompt:", error)
setManualImportError(
"Could not copy prompt. Select and copy it manually.",
)
}
}
const handleManualMemoryImportSave = async () => {
if (!manualImportProvider) return
const content = normalizeManualMemoryImport(manualImportText)
if (!content) {
setManualImportError("Paste the exported memories first.")
return
}
setManualImportSaving(true)
setManualImportError("")
try {
const providerConfig = manualImportProviderConfig[manualImportProvider]
const response = await chrome.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
actionSource: providerConfig.actionSource,
data: {
content,
title: `${providerConfig.label} memories import`,
},
})
if (!response?.success) {
throw new Error(response?.error || "Could not add memories")
}
setManualImportSaved(true)
window.setTimeout(() => {
handleCloseManualMemoryImport()
}, 1000)
} catch (error) {
console.error("Failed to add manual memory import:", error)
setManualImportError(
error instanceof Error ? error.message : "Could not add memories",
)
} finally {
setManualImportSaving(false)
}
}
const handleSignOut = async () => { const handleSignOut = async () => {
try { try {
await Promise.all([ await Promise.all([
@ -298,7 +637,7 @@ function App() {
> >
<img <img
alt="supermemory" alt="supermemory"
src="./icon-48.png" src="./new_logo.png"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]" className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]"
/> />
</div> </div>
@ -306,11 +645,9 @@ function App() {
<span className="text-[11px] font-medium text-[#737373] leading-normal"> <span className="text-[11px] font-medium text-[#737373] leading-normal">
Your Your
</span> </span>
<img <span className="text-[15px] font-semibold leading-none text-white">
alt="supermemory" supermemory
src="./logo-fullmark.svg" </span>
className="h-[14.5px] w-auto"
/>
</div> </div>
</div> </div>
</div> </div>
@ -364,7 +701,7 @@ function App() {
> >
<img <img
alt="supermemory" alt="supermemory"
src="./icon-48.png" src="./new_logo.png"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]" className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[29px] h-[29px]"
/> />
</div> </div>
@ -378,11 +715,9 @@ function App() {
return name.endsWith("s") ? `${name}'` : `${name}'s` return name.endsWith("s") ? `${name}'` : `${name}'s`
})()} })()}
</span> </span>
<img <span className="text-[15px] font-semibold leading-none text-white">
alt="supermemory" supermemory
src="./logo-fullmark.svg" </span>
className="h-[14.5px] w-auto"
/>
</div> </div>
</div> </div>
{userSignedIn && ( {userSignedIn && (
@ -637,140 +972,247 @@ function App() {
{saving ? "Saving..." : "Add to supermemory"} {saving ? "Saving..." : "Add to supermemory"}
</button> </button>
{saveError && (
<p className="mt-2 text-xs leading-snug text-red-300">
{saveError}
</p>
)}
</div> </div>
</div> </div>
) : activeTab === "imports" ? ( ) : activeTab === "imports" ? (
<div className="flex flex-col gap-4 min-h-[200px]"> <div className="flex flex-col gap-4 min-h-[200px]">
{/* Import Actions */} {manualImportProvider ? (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-3">
<div className="flex flex-col gap-2"> <div className="flex items-start justify-between gap-3">
<div>
<h3 className="m-0 text-base font-semibold text-white">
Import{" "}
{
manualImportProviderConfig[manualImportProvider]
.label
}{" "}
memories
</h3>
<p className="m-0 mt-1 text-xs leading-tight text-[#737373]">
Copy the prompt, paste the response here, then add it
to supermemory.
</p>
</div>
<button <button
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-start transition-colors duration-200 hover:bg-[#5B7EF520]" aria-label="Close manual import"
style={{ className="shrink-0 bg-transparent border-none cursor-pointer p-1 text-[#737373] transition-colors hover:text-white"
boxShadow: onClick={handleCloseManualMemoryImport}
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset", type="button"
>
<svg
aria-hidden="true"
fill="none"
height="18"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="18"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-sm font-medium text-white">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-black text-xs">
1
</span>
<span>Copy this prompt into chat</span>
</div>
<div
className="relative overflow-hidden rounded-xl bg-black/70 p-3"
style={{ boxShadow: cardShadow }}
>
<pre className="m-0 max-h-28 overflow-y-auto whitespace-pre-wrap pb-9 pr-1 text-xs leading-snug text-[#B7B7B7] font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif]">
{manualMemoryImportPrompt}
</pre>
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-14 bg-linear-to-t from-black/80 to-transparent" />
<button
className="absolute bottom-3 right-3 flex items-center gap-1.5 rounded-lg border-none bg-[#FFFFFF1A] px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-[#FFFFFF26]"
onClick={handleCopyManualImportPrompt}
type="button"
>
<svg
aria-hidden="true"
fill="none"
height="14"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="14"
>
<rect
height="14"
rx="2"
ry="2"
width="14"
x="8"
y="8"
/>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
{manualImportCopied ? "Copied" : "Copy"}
</button>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-sm font-medium text-white">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-black text-xs">
2
</span>
<span>Paste results below</span>
</div>
<textarea
className="min-h-32 w-full resize-none rounded-xl border border-[#FFFFFF14] bg-[#FFFFFF08] p-3 text-sm leading-snug text-white outline-none placeholder:text-[#737373] focus:border-[#5B7EF566]"
onChange={(event) => {
setManualImportText(event.target.value)
setManualImportError("")
}} }}
placeholder="Paste your memory details here"
value={manualImportText}
/>
</div>
{manualImportError && (
<p className="m-0 text-xs leading-tight text-red-300">
{manualImportError}
</p>
)}
<div className="flex justify-end gap-2 pt-1">
<button
className="rounded-lg border-none bg-transparent px-4 py-2 text-sm font-medium text-[#8A8C90] transition-colors hover:bg-[#FFFFFF0D] hover:text-white"
onClick={handleCloseManualMemoryImport}
type="button"
>
Cancel
</button>
<button
className="rounded-xl border-none px-3.5 py-2 text-xs font-medium text-white transition-opacity disabled:cursor-not-allowed disabled:opacity-80"
style={{
background:
"linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
boxShadow:
"1px 1px 2px 0px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)",
}}
disabled={manualImportSaving || manualImportSaved}
onClick={handleManualMemoryImportSave}
type="button"
>
<span className="flex items-center gap-2 whitespace-nowrap">
{manualImportSaved ? (
"Done"
) : (
<>
<svg
aria-hidden="true"
className="h-4 w-5 shrink-0"
fill="none"
viewBox="0 0 20 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.4295 6.3108H12.1691V0H9.82324V6.84734C9.82324 7.57459 10.1103 8.27304 10.6206 8.78766L16.549 14.7664L18.2077 13.0936L13.8291 8.6779H19.4309V6.31219L19.4295 6.3108Z"
fill="currentColor"
/>
<path
d="M1.08945 2.90808L5.46808 7.32387H-0.133789V9.68958H7.12669V16.0003H9.4725V9.15304C9.4725 8.42574 9.18541 7.72728 8.67512 7.21272L2.74809 1.23535L1.08945 2.90808Z"
fill="currentColor"
/>
</svg>
{manualImportSaving
? "Saving..."
: "Save to supermemory"}
</>
)}
{manualImportSaved && (
<svg
aria-hidden="true"
fill="none"
height="14"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.4"
viewBox="0 0 24 24"
width="14"
>
<path d="M20 6 9 17l-5-5" />
</svg>
)}
</span>
</button>
</div>
</div>
) : showChatAppImports ? (
<div className="flex flex-col gap-3">
<ImportCard
icon={<ClaudeLogo className="w-4 h-4 shrink-0" />}
title="Import Claude Memories"
description="Open 'view and manage' > save your memories to supermemory"
onClick={() => {
chrome.tabs.create({
url: "https://claude.ai/settings/capabilities",
})
}}
/>
<ImportCard
icon={<OpenAILogo className="w-3 h-3.5 shrink-0" />}
title="Import ChatGPT Memories"
description="Open 'manage' > save your memories to supermemory"
onClick={() => { onClick={() => {
chrome.tabs.create({ chrome.tabs.create({
url: "https://chatgpt.com/#settings/Personalization", url: "https://chatgpt.com/#settings/Personalization",
}) })
}} }}
type="button" />
> <ImportCard
<div className="text-left"> icon={<GrokLogo className="w-4 h-4 shrink-0" />}
<p className="flex items-center gap-2 font-medium"> title="Import Grok Memories"
<svg description="Open 'Memory from your chats' > save your memories to supermemory"
aria-label="ChatGPT Logo" onClick={() => {
className="w-3 h-3.5 shrink-0" chrome.tabs.create({
fill="currentColor" url: "https://grok.com/?_s=data&sm_grok_import=memories",
role="img" })
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
Import ChatGPT Memories
</p>
<p className="m-0 text-[14px] text-[#737373] leading-tight">
open 'manage' &gt; save your memories to supermemory
</p>
</div>
<RightArrow className="size-4" />
</button>
</div>
<div className="flex flex-col gap-2">
<button
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-start transition-colors duration-200 outline-none appearance-none hover:bg-[#5B7EF520] focus:outline-none"
style={{
boxShadow:
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset",
}} }}
onClick={async () => { />
const targetUrl = "https://x.com/i/bookmarks" <ImportCard
icon={
try { <GeminiLogo className="w-4 h-4 shrink-0 rounded-[4px]" />
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true,
})
const isOnBookmarksPage =
activeTab?.url?.includes("x.com/i/bookmarks") ||
activeTab?.url?.includes("twitter.com/i/bookmarks")
if (isOnBookmarksPage && activeTab?.id) {
try {
await chrome.tabs.sendMessage(activeTab.id, {
action: MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL,
})
} catch (error) {
// Content script may not be loaded yet, fall back to intent-based approach
console.error(
"Failed to send message to content script:",
error,
)
const intentExpiry =
Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
await chrome.storage.local.set({
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]:
intentExpiry,
})
await chrome.tabs.create({
url: targetUrl,
})
} }
} else { title="Import Gemini Memories"
const intentExpiry = description="Paste memories exported from Gemini chat"
Date.now() + UI_CONFIG.IMPORT_INTENT_TTL onClick={() => handleOpenManualMemoryImport("gemini")}
await chrome.storage.local.set({ />
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]:
intentExpiry,
})
await chrome.tabs.create({
url: targetUrl,
})
}
} catch (error) {
console.error("Error opening Twitter import:", error)
// Fallback: try to open the bookmarks page anyway
try {
await chrome.tabs.create({
url: targetUrl,
})
} catch (fallbackError) {
console.error(
"Failed to open bookmarks page:",
fallbackError,
)
}
}
}}
type="button"
>
<div className="text-left">
<p className="flex items-center gap-2 font-medium">
<svg
aria-label="X Twitter Logo"
className="w-3 h-3.5 shrink-0"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>X Twitter Logo</title>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Import X/Twitter Bookmarks
</p>
<p className="m-0 text-[14px] text-[#737373] leading-tight">
Opens import dialog automatically
</p>
</div>
<RightArrow className="size-4" />
</button>
</div> </div>
) : (
<div className="flex flex-col gap-4">
<ImportCard
icon={<ChatAppsLogo />}
title="Import Chat Memories"
description="Import your ChatGPT, Claude, Grok, and Gemini memories"
onClick={() => setShowChatAppImports(true)}
/>
<ImportCard
icon={<XLogo className="w-3 h-3.5 shrink-0" />}
title="Import X/Twitter Bookmarks"
description="Opens import dialog automatically"
onClick={handleTwitterBookmarksImport}
/>
</div> </div>
)}
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4 min-h-[200px] pl-1"> <div className="flex flex-col gap-4 min-h-[200px] pl-1">
@ -873,13 +1315,13 @@ function App() {
</h2> </h2>
<ul className="list-none p-0 m-0 text-left"> <ul className="list-none p-0 m-0 text-left">
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-[''] before:absolute before:left-0 before:text-neutral-500 before:font-bold"> <li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Save any page to your supermemory Save any page to your supermemory
</li> </li>
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-[''] before:absolute before:left-0 before:text-neutral-500 before:font-bold"> <li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Import all your Twitter / X Bookmarks Import all your Twitter / X Bookmarks
</li> </li>
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-[''] before:absolute before:left-0 before:text-neutral-500 before:font-bold"> <li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['-'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Import your ChatGPT Memories Import your ChatGPT Memories
</li> </li>
</ul> </ul>
@ -904,9 +1346,7 @@ function App() {
className="w-full py-3 px-6 bg-[#2d3f5c] text-white border-none rounded-3xl text-base font-medium cursor-pointer transition-colors duration-200 hover:bg-[#3d5270] disabled:bg-neutral-600 disabled:cursor-not-allowed" className="w-full py-3 px-6 bg-[#2d3f5c] text-white border-none rounded-3xl text-base font-medium cursor-pointer transition-colors duration-200 hover:bg-[#3d5270] disabled:bg-neutral-600 disabled:cursor-not-allowed"
onClick={() => { onClick={() => {
chrome.tabs.create({ chrome.tabs.create({
url: import.meta.env.PROD url: getSupermemoryLoginUrl(),
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
}) })
}} }}
type="button" type="button"

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Default Popup Title</title> <title>supermemory</title>
<meta name="manifest.type" content="browser_action" /> <meta name="manifest.type" content="browser_action" />
</head> </head>
<body> <body>

View file

@ -1,105 +1,114 @@
import { getSupermemoryLoginUrl } from "../../utils/constants"
const featureCards = [
{
number: "01",
title: "Save any page",
description: "Articles, docs, and references from the browser.",
},
{
number: "02",
title: "Import X bookmarks",
description: "Bring saved posts into your memory library.",
},
{
number: "03",
title: "Capture AI chats",
description: "Save useful conversations from ChatGPT, Claude, and Gemini.",
},
{
number: "04",
title: "Use context anywhere",
description: "Search and reuse memories when you need them.",
},
]
function Welcome() { function Welcome() {
return ( 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="relative min-h-screen overflow-hidden bg-[#05080D] text-white font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif]">
<div className="max-w-4xl w-full text-center"> <div
{/* Header */} className="pointer-events-none absolute inset-0"
<div className="mb-12"> style={{
<img background:
alt="supermemory" "linear-gradient(180deg, #05080D 0%, #05070A 48%, #060A18 100%)",
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"> <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.20)_1px,transparent_1px)] bg-size-[32px_32px] opacity-70 mask-[linear-gradient(to_bottom,transparent_0%,black_12%,black_100%)]" />
Your AI second brain for saving and organizing everything that <div className="pointer-events-none absolute inset-x-0 bottom-0 h-[55%] bg-[radial-gradient(ellipse_at_bottom,rgba(20,65,255,0.42),transparent_68%)]" />
matters. Supermemory learns and remembers everything you save, your
preferences, and understands you.
</p>
</div>
{/* Features Section */} <main className="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-6 sm:px-10">
<div className="mb-12"> <header className="flex items-center border-b border-white/10 pb-5">
<h2 className="text-2xl font-semibold text-black mb-8"> <div className="flex items-center gap-2">
What can you do with supermemory ? <img alt="" className="size-8 rounded-[4px]" src="./new_logo.png" />
</h2> <span className="text-lg font-semibold leading-none text-white">
supermemory
</span>
</div>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8"> <section className="flex flex-1 flex-col items-center justify-center py-10 text-center">
<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="mx-auto max-w-3xl">
<div className="text-3xl mb-4 block">💾</div> <h1 className="text-4xl font-semibold leading-[1.05] tracking-normal text-white sm:text-6xl">
<h3 className="text-lg font-semibold text-black mb-3"> Your browser now has{" "}
Save Any Page <span className="text-[#369BFD]">supermemory.</span>
</h3> </h1>
<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="mt-8 flex flex-col justify-center gap-3 sm:flex-row">
<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 <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" className="h-12 rounded-xl px-7 text-sm font-semibold text-white transition hover:brightness-110 focus:outline-none focus:ring-2 focus:ring-[#36fdfd]/70"
style={{
background:
"linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
boxShadow:
"1px 1px 2px 0px #1A88FF inset, 0 2px 18px 0 rgba(54, 155, 253, 0.24)",
}}
onClick={() => { onClick={() => {
chrome.tabs.create({ chrome.tabs.create({
url: import.meta.env.PROD url: getSupermemoryLoginUrl(),
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
}) })
}} }}
type="button" type="button"
> >
Login to Get started Sign in to connect
</button>
<button
className="h-12 rounded-xl border border-[#369BFD]/25 bg-[#080B0F]/80 px-6 text-sm font-semibold text-[#C7D7F2] transition hover:border-[#369BFD]/50 hover:bg-[#0D121A] focus:outline-none focus:ring-2 focus:ring-[#369BFD]/30"
onClick={() => {
chrome.tabs.create({
url: "https://supermemory.ai",
})
}}
type="button"
>
Open supermemory.ai
</button> </button>
</div> </div>
</div>
{/* Footer */} <div className="mt-14 grid w-full max-w-5xl gap-3 text-left sm:grid-cols-2 lg:grid-cols-4">
<div className="border-t border-gray-200 pt-6 mt-8"> {featureCards.map((feature) => (
<p className="text-sm text-gray-600"> <div
Learn more at{" "} className="rounded-lg border border-white/10 bg-white/[0.035] p-4"
<a key={feature.number}
className="text-blue-500 no-underline hover:underline hover:text-blue-700"
href="https://supermemory.ai"
rel="noopener noreferrer"
target="_blank"
> >
supermemory.ai <p className="text-[11px] font-medium text-[#737373]">
</a> {feature.number}
</p>
<h2 className="mt-4 text-sm font-semibold text-white">
{feature.title}
</h2>
<p className="mt-2 text-sm leading-6 text-[#A1A1AA]">
{feature.description}
</p> </p>
</div> </div>
))}
</div> </div>
</section>
<footer className="border-t border-white/10 py-5 text-xs text-[#737373]">
supermemory stores your extension session locally in Chrome.
</footer>
</main>
</div> </div>
) )
} }

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon-16.png" /> <link rel="icon" type="image/png" href="/new_logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to supermemory</title> <title>Welcome to supermemory</title>
</head> </head>

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -4,5 +4,6 @@
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"types": ["chrome"] "types": ["chrome"]
} },
"exclude": ["**/*.test.ts"]
} }

View file

@ -3,6 +3,7 @@
*/ */
import { API_ENDPOINTS } from "./constants" import { API_ENDPOINTS } from "./constants"
import { bearerToken, defaultProject, userData } from "./storage" import { bearerToken, defaultProject, userData } from "./storage"
import { buildSearchMemoriesBody } from "./search-request"
import { import {
AuthenticationError, AuthenticationError,
type MemoryPayload, type MemoryPayload,
@ -145,14 +146,14 @@ export async function saveMemory(payload: MemoryPayload): Promise<unknown> {
/** /**
* Search memories using Supermemory API * Search memories using Supermemory API
*/ */
export async function searchMemories(query: string): Promise<unknown> { export async function searchMemories(
query: string,
containerTag?: string,
): Promise<unknown> {
try { try {
const response = await makeAuthenticatedRequest<unknown>("/v4/search", { const response = await makeAuthenticatedRequest<unknown>("/v4/search", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify(buildSearchMemoriesBody(query, containerTag)),
q: query,
include: { relatedMemories: true },
}),
}) })
return response return response
} catch (error) { } catch (error) {

View file

@ -10,6 +10,17 @@ export const API_ENDPOINTS = {
: "http://localhost:3000", : "http://localhost:3000",
} as const } as const
export function getSupermemoryLoginUrl(): string {
const baseUrl = API_ENDPOINTS.SUPERMEMORY_WEB
const loginUrl = new URL("/login", baseUrl)
const redirectUrl = new URL("/", baseUrl)
redirectUrl.searchParams.set("extension-auth-success", "true")
loginUrl.searchParams.set("redirect", redirectUrl.toString())
return loginUrl.toString()
}
/** /**
* DOM Element IDs * DOM Element IDs
*/ */
@ -22,6 +33,7 @@ export const ELEMENT_IDS = {
SAVE_TWEET_ELEMENT: "sm-save-tweet-element", SAVE_TWEET_ELEMENT: "sm-save-tweet-element",
CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element", CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element",
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element", CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
GEMINI_INPUT_BAR_ELEMENT: "sm-gemini-input-bar-element",
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element", T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
PROJECT_SELECTION_MODAL: "sm-project-selection-modal", PROJECT_SELECTION_MODAL: "sm-project-selection-modal",
} as const } as const
@ -58,6 +70,8 @@ export const DOMAINS = {
TWITTER: ["x.com", "twitter.com"], TWITTER: ["x.com", "twitter.com"],
CHATGPT: ["chatgpt.com", "chat.openai.com"], CHATGPT: ["chatgpt.com", "chat.openai.com"],
CLAUDE: ["claude.ai"], CLAUDE: ["claude.ai"],
GROK: ["grok.com", "x.ai"],
GEMINI: ["gemini.google.com"],
T3: ["t3.chat"], T3: ["t3.chat"],
SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"], SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"],
} as const } as const
@ -94,6 +108,8 @@ export const POSTHOG_EVENT_KEY = {
T3_CHAT_MEMORIES_AUTO_SEARCHED: "t3_chat_memories_auto_searched", T3_CHAT_MEMORIES_AUTO_SEARCHED: "t3_chat_memories_auto_searched",
CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched", CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched",
CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched", CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched",
GEMINI_CHAT_MEMORIES_SEARCHED: "gemini_chat_memories_searched",
GEMINI_CHAT_MEMORIES_AUTO_SEARCHED: "gemini_chat_memories_auto_searched",
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched", CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched", CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched",
} as const } as const

View file

@ -39,7 +39,6 @@ export function createRouteDetection(
const checkForRouteChange = () => { const checkForRouteChange = () => {
if (window.location.href !== currentUrl) { if (window.location.href !== currentUrl) {
currentUrl = window.location.href currentUrl = window.location.href
console.log(`${config.platform} route changed, re-initializing`)
setTimeout(config.reinitCallback, 1000) setTimeout(config.reinitCallback, 1000)
} }
} }

View file

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

View file

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

View file

@ -51,7 +51,6 @@ export async function captureTwitterTokens(
if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) { if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) {
const tokensAlreadyLogged = await getTokensLogged() const tokensAlreadyLogged = await getTokensLogged()
if (!tokensAlreadyLogged) { if (!tokensAlreadyLogged) {
console.log("Twitter auth tokens captured successfully")
await setTokensLogged() await setTokensLogged()
} }

View file

@ -0,0 +1,58 @@
import { describe, expect, it } from "bun:test"
import { expandTweetText } from "./twitter-utils"
const link = (url: string, expanded_url: string, display_url: string) => ({
url,
expanded_url,
display_url,
indices: [0, url.length] as [number, number],
})
describe("expandTweetText", () => {
it("returns the text unchanged when there are no url entities", () => {
expect(expandTweetText("just text", undefined)).toBe("just text")
expect(expandTweetText("just text", [])).toBe("just text")
})
it("replaces a t.co shortlink with a markdown link to the expanded url", () => {
const text = "check this https://t.co/abc123 out"
const urls = [
link(
"https://t.co/abc123",
"https://example.com/article",
"example.com/article",
),
]
expect(expandTweetText(text, urls)).toBe(
"check this [example.com/article](https://example.com/article) out",
)
})
it("expands multiple shortlinks including repeats", () => {
const text = "a https://t.co/aaa b https://t.co/bbb c https://t.co/aaa"
const urls = [
link("https://t.co/aaa", "https://a.com", "a.com"),
link("https://t.co/bbb", "https://b.com", "b.com"),
]
expect(expandTweetText(text, urls)).toBe(
"a [a.com](https://a.com) b [b.com](https://b.com) c [a.com](https://a.com)",
)
})
it("falls back to the expanded url as label when display_url is empty", () => {
const text = "see https://t.co/xyz"
const urls = [link("https://t.co/xyz", "https://long.example.com/path", "")]
expect(expandTweetText(text, urls)).toBe(
"see [https://long.example.com/path](https://long.example.com/path)",
)
})
it("skips entries missing a url or expanded_url", () => {
const text = "keep https://t.co/keep here"
const urls = [
link("", "https://nope.com", "nope.com"),
link("https://t.co/keep", "", "keep.com"),
]
expect(expandTweetText(text, urls)).toBe("keep https://t.co/keep here")
})
})

View file

@ -113,19 +113,14 @@ export class TwitterImporter {
const headers = createTwitterAPIHeaders(tokens) const headers = createTwitterAPIHeaders(tokens)
// Build API request with pagination // Build API request with pagination
const variables = const collectionId = this.config.isFolderImport
this.config.isFolderImport && this.config.bookmarkCollectionId ? this.config.bookmarkCollectionId
? buildBookmarkCollectionVariables(this.config.bookmarkCollectionId) : undefined
const variables = collectionId
? buildBookmarkCollectionVariables(collectionId, cursor)
: buildRequestVariables(cursor) : buildRequestVariables(cursor)
const urlWithCursor = cursor const baseUrl = collectionId ? BOOKMARK_COLLECTION_URL : BOOKMARKS_URL
? `${ const urlWithCursor = `${baseUrl}&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))}`
: 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, { const response = await fetch(urlWithCursor, {
method: "GET", method: "GET",
@ -186,8 +181,6 @@ export class TwitterImporter {
if (documents.length > 0) { if (documents.length > 0) {
await saveAllTweets(documents) await saveAllTweets(documents)
} }
console.log("Tweets saved")
console.log("Documents:", documents)
} catch (error) { } catch (error) {
console.error("Error saving tweets batch:", error) console.error("Error saving tweets batch:", error)
await this.config.onError(error as Error) await this.config.onError(error as Error)
@ -201,10 +194,7 @@ export class TwitterImporter {
[] []
const nextCursor = extractNextCursor(instructions) const nextCursor = extractNextCursor(instructions)
console.log("Next cursor:", nextCursor) if (nextCursor && tweets.length > 0) {
console.log("Tweets length:", tweets.length)
if (nextCursor && tweets.length > 0 && !this.config.isFolderImport) {
await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
await this.batchImportAll(nextCursor, importedCount, uniqueGroupId) await this.batchImportAll(nextCursor, importedCount, uniqueGroupId)
} else { } else {

View file

@ -56,13 +56,45 @@ interface MediaEntity {
} }
} }
video_info?: { video_info?: {
variants?: Array<{ variants?: VideoVariant[]
url: string
}>
duration_millis?: number duration_millis?: number
} }
} }
export interface VideoVariant {
url: string
bitrate?: number
content_type?: string
}
/**
* Twitter returns several video variants for a single video: an HLS `.m3u8`
* playlist (no bitrate) plus multiple `video/mp4` renditions at different
* bitrates, in no guaranteed order. Taking `variants[0]` therefore often stored
* the HLS playlist URL (not a directly usable file) or the lowest-quality clip.
* Pick the highest-bitrate MP4 instead, falling back to the first variant when
* no MP4 rendition is present.
*/
export function pickBestVideoVariantUrl(
variants: VideoVariant[] | undefined,
): string {
if (!variants || variants.length === 0) return ""
const mp4s = variants.filter(
(v) => v.content_type === "video/mp4" || /\.mp4(?:\?|$)/i.test(v.url),
)
const pool = mp4s.length > 0 ? mp4s : variants
let best = pool[0]
for (const variant of pool) {
if ((variant.bitrate ?? 0) > (best?.bitrate ?? 0)) {
best = variant
}
}
return best?.url || ""
}
export interface Tweet { export interface Tweet {
__typename?: string __typename?: string
lang?: string lang?: string
@ -257,7 +289,7 @@ export function transformTweetData(
const videos = media const videos = media
.filter((m) => m.type === "video") .filter((m) => m.type === "video")
.map((m) => ({ .map((m) => ({
url: m.video_info?.variants?.[0]?.url || "", url: pickBestVideoVariantUrl(m.video_info?.variants),
thumbnail_url: m.media_url_https, thumbnail_url: m.media_url_https,
duration: m.video_info?.duration_millis || 0, duration: m.video_info?.duration_millis || 0,
})) }))
@ -367,6 +399,27 @@ export function extractNextCursor(
return null return null
} }
/**
* Tweet `full_text` embeds links as opaque `t.co` shortlinks, while
* `entities.urls` carries the real destination. Replace each shortlink with a
* markdown link to its expanded URL (labelled with the human-readable
* display_url) so imported tweets keep working, searchable links instead of
* `https://t.co/xxxx`.
*/
export function expandTweetText(
text: string,
urls: Tweet["entities"]["urls"],
): string {
if (!urls || urls.length === 0) return text
let expanded = text
for (const link of urls) {
if (!link?.url || !link.expanded_url) continue
const label = link.display_url || link.expanded_url
expanded = expanded.split(link.url).join(`[${label}](${link.expanded_url})`)
}
return expanded
}
/** /**
* Convert Tweet object to markdown format for storage * Convert Tweet object to markdown format for storage
*/ */
@ -380,8 +433,8 @@ export function tweetToMarkdown(tweet: Tweet): string {
markdown += `**Date:** ${date} ${time}\n` markdown += `**Date:** ${date} ${time}\n`
markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n` markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`
// Add tweet text // Add tweet text with t.co shortlinks expanded to their real destinations
markdown += `${tweet.text}\n\n` markdown += `${expandTweetText(tweet.text, tweet.entities.urls)}\n\n`
// Add media if present // Add media if present
if (tweet.photos && tweet.photos.length > 0) { if (tweet.photos && tweet.photos.length > 0) {
@ -434,9 +487,18 @@ export function buildRequestVariables(cursor?: string, count = 100) {
/** /**
* Build Twitter API request variables for bookmark collection * Build Twitter API request variables for bookmark collection
*/ */
export function buildBookmarkCollectionVariables(bookmarkCollectionId: string) { export function buildBookmarkCollectionVariables(
return { bookmarkCollectionId: string,
cursor?: string,
) {
const variables: Record<string, unknown> = {
bookmark_collection_id: bookmarkCollectionId, bookmark_collection_id: bookmarkCollectionId,
includePromotedContent: true, includePromotedContent: true,
} }
if (cursor) {
variables.cursor = cursor
}
return variables
} }

View file

@ -0,0 +1,75 @@
import { describe, expect, it } from "bun:test"
import { pickBestVideoVariantUrl } from "./twitter-utils"
describe("pickBestVideoVariantUrl", () => {
it("returns an empty string when there are no variants", () => {
expect(pickBestVideoVariantUrl(undefined)).toBe("")
expect(pickBestVideoVariantUrl([])).toBe("")
})
it("picks the highest-bitrate mp4, not the first variant", () => {
const variants = [
{
url: "https://video.twimg.com/playlist.m3u8",
content_type: "application/x-mpegURL",
},
{
url: "https://video.twimg.com/low.mp4",
content_type: "video/mp4",
bitrate: 256000,
},
{
url: "https://video.twimg.com/high.mp4",
content_type: "video/mp4",
bitrate: 2176000,
},
{
url: "https://video.twimg.com/mid.mp4",
content_type: "video/mp4",
bitrate: 832000,
},
]
expect(pickBestVideoVariantUrl(variants)).toBe(
"https://video.twimg.com/high.mp4",
)
})
it("does not return the HLS playlist when mp4 renditions exist", () => {
const variants = [
{
url: "https://video.twimg.com/playlist.m3u8",
content_type: "application/x-mpegURL",
},
{
url: "https://video.twimg.com/only.mp4",
content_type: "video/mp4",
bitrate: 632000,
},
]
expect(pickBestVideoVariantUrl(variants)).toBe(
"https://video.twimg.com/only.mp4",
)
})
it("falls back to the first variant when no mp4 is present", () => {
const variants = [
{
url: "https://video.twimg.com/playlist.m3u8",
content_type: "application/x-mpegURL",
},
]
expect(pickBestVideoVariantUrl(variants)).toBe(
"https://video.twimg.com/playlist.m3u8",
)
})
it("detects mp4 by extension when content_type is absent", () => {
const variants = [
{ url: "https://video.twimg.com/240/vid.mp4?tag=12" },
{ url: "https://video.twimg.com/720/vid.mp4?tag=12", bitrate: 2176000 },
]
expect(pickBestVideoVariantUrl(variants)).toBe(
"https://video.twimg.com/720/vid.mp4?tag=12",
)
})
})

View file

@ -38,6 +38,9 @@ export interface MemoryData {
url?: string url?: string
ogImage?: string ogImage?: string
title?: string title?: string
sourcePlatform?: string
sourcePlatformLabel?: string
sourceSurface?: string
} }
/** /**

View file

@ -117,7 +117,7 @@ export function createToast(state: ToastState): HTMLElement {
break break
case "success": { case "success": {
const iconUrl = browser.runtime.getURL("/icon-16.png") const iconUrl = browser.runtime.getURL("/new_logo.png")
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />` icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
textElement.textContent = "Added to Memory" textElement.textContent = "Added to Memory"
break break
@ -184,7 +184,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
` `
const iconUrl = browser.runtime.getURL("/icon-16.png") const iconUrl = browser.runtime.getURL("/new_logo.png")
button.style.backgroundImage = `url("${iconUrl}")` button.style.backgroundImage = `url("${iconUrl}")`
button.style.backgroundRepeat = "no-repeat" button.style.backgroundRepeat = "no-repeat"
@ -232,7 +232,7 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
z-index: 1000; z-index: 1000;
` `
const iconFileName = "/icon-16.png" const iconFileName = "/new_logo.png"
const iconUrl = browser.runtime.getURL(iconFileName) const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = ` iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
@ -261,31 +261,72 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
* @returns HTMLElement - The save button element * @returns HTMLElement - The save button element
*/ */
export function createChatGPTInputBarElement(onClick: () => void): HTMLElement { export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
const iconButton = document.createElement("div") return createConnectedIndicator(onClick)
}
export function createConnectedIndicator(onClick: () => void): HTMLElement {
const iconButton = document.createElement("button")
iconButton.type = "button"
iconButton.setAttribute("aria-label", "supermemory connected")
iconButton.dataset.supermemoryConnectedIndicator = "true"
iconButton.style.cssText = ` iconButton.style.cssText = `
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: auto; width: 32px;
height: 24px; height: 32px;
min-width: 32px;
cursor: pointer; cursor: pointer;
transition: opacity 0.2s ease; transition: opacity 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
border-radius: 50%; border-radius: 50%;
border: none;
background: transparent;
padding: 0;
position: relative;
flex-shrink: 0;
` `
// Use appropriate icon based on theme const iconFileName = "/new_logo.png"
const iconFileName = "/icon-16.png"
const iconUrl = browser.runtime.getURL(iconFileName) const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = ` iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 50%;" /> <img src="${iconUrl}" width="20" height="20" alt="" style="border-radius: 5px; display: block;" />
` `
const tooltip = document.createElement("div")
tooltip.textContent = "supermemory connected"
tooltip.style.cssText = `
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(2px);
background: #0A0E14;
color: #FAFAFA;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
padding: 6px 8px;
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.16s ease, transform 0.16s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
z-index: 2147483647;
`
iconButton.appendChild(tooltip)
iconButton.addEventListener("mouseenter", () => { iconButton.addEventListener("mouseenter", () => {
iconButton.style.opacity = "0.8" iconButton.style.backgroundColor = "rgba(255, 255, 255, 0.08)"
tooltip.style.opacity = "1"
tooltip.style.transform = "translateX(-50%) translateY(0)"
}) })
iconButton.addEventListener("mouseleave", () => { iconButton.addEventListener("mouseleave", () => {
iconButton.style.opacity = "1" iconButton.style.backgroundColor = "transparent"
tooltip.style.opacity = "0"
tooltip.style.transform = "translateX(-50%) translateY(2px)"
}) })
iconButton.addEventListener("click", (event) => { iconButton.addEventListener("click", (event) => {
@ -303,42 +344,11 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
* @returns HTMLElement - The save button element * @returns HTMLElement - The save button element
*/ */
export function createClaudeInputBarElement(onClick: () => void): HTMLElement { export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
const iconButton = document.createElement("div") return createConnectedIndicator(onClick)
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" export function createGeminiInputBarElement(onClick: () => void): HTMLElement {
const iconUrl = browser.runtime.getURL(iconFileName) return createConnectedIndicator(onClick)
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
} }
/** /**
@ -360,7 +370,7 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
background: transparent; background: transparent;
` `
const iconFileName = "/icon-16.png" const iconFileName = "/new_logo.png"
const iconUrl = browser.runtime.getURL(iconFileName) const iconUrl = browser.runtime.getURL(iconFileName)
iconButton.innerHTML = ` iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" /> <img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
@ -433,7 +443,7 @@ export function createProjectSelectionModal(
margin-bottom: 20px; margin-bottom: 20px;
` `
const iconUrl = browser.runtime.getURL("/icon-16.png") const iconUrl = browser.runtime.getURL("/new_logo.png")
header.innerHTML = ` header.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;"> <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;"> <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #ffffff; display: flex; align-items: center; gap: 8px;">
@ -702,7 +712,7 @@ export const DOMUtils = {
if (icon && text) { if (icon && text) {
if (state === "success") { if (state === "success") {
const iconUrl = browser.runtime.getURL("/icon-16.png") const iconUrl = browser.runtime.getURL("/new_logo.png")
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />` icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
icon.style.animation = "" icon.style.animation = ""
text.textContent = "Added to Memory" text.textContent = "Added to Memory"

View file

@ -29,7 +29,7 @@ export default defineConfig({
manifest: { manifest: {
name: "supermemory", name: "supermemory",
homepage_url: "https://supermemory.ai", homepage_url: "https://supermemory.ai",
version: "6.1.4", version: "6.1.3",
permissions: ["storage", "activeTab", "webRequest", "tabs"], permissions: ["storage", "activeTab", "webRequest", "tabs"],
host_permissions: [ host_permissions: [
"*://x.com/*", "*://x.com/*",
@ -38,11 +38,18 @@ export default defineConfig({
"*://api.supermemory.ai/*", "*://api.supermemory.ai/*",
"*://chatgpt.com/*", "*://chatgpt.com/*",
"*://chat.openai.com/*", "*://chat.openai.com/*",
"*://grok.com/*",
"*://*.grok.com/*",
"*://x.ai/*",
"*://*.x.ai/*",
"*://claude.ai/*",
"*://gemini.google.com/*",
"*://t3.chat/*",
"https://*.posthog.com/*", "https://*.posthog.com/*",
], ],
web_accessible_resources: [ web_accessible_resources: [
{ {
resources: ["icon-16.png", "fonts/*.ttf"], resources: ["new_logo.png", "fonts/*.ttf"],
matches: ["<all_urls>"], matches: ["<all_urls>"],
}, },
], ],

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

View file

@ -0,0 +1,99 @@
---
title: "Automations and Proactiveness"
sidebarTitle: "Automations"
description: "Scheduled work Company Brain runs on its own, and when it speaks without being asked"
icon: "bot"
---
import { SlackThread, SlackMessage, Mention, ChannelRef } from "/snippets/slack-message.mdx";
Company Brain doesn't only answer when you @mention it. It can run recurring work on a schedule, and it can speak in a thread on its own when it has something genuinely worth saying. Both are opt-in, both are rate-limited, and both read from exactly the same [permissions graph](/company-brain/permissions) as a normal question — neither is a backdoor around it.
## Automations
An automation is a prompt that runs on a schedule and posts the result somewhere. You write it once, in plain language:
<SlackThread channel="#product">
<SlackMessage self hasAvatar time="9:03 AM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> every Monday at 9am, post a digest of what shipped last week and what's still open, to <ChannelRef>product</ChannelRef>.
</SlackMessage>
<SlackMessage bot hasAvatar time="9:03 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Got it — scheduled. First digest posts Monday, 9:00 AM, to <ChannelRef>product</ChannelRef>.
</SlackMessage>
</SlackThread>
<Steps>
<Step title="Schedule fires">
The automation wakes up at its set time — no one has to trigger it.
</Step>
<Step title="Gathers context">
It reads using only **org-shared** connections and channel memory — never a person's personal credentials, even if the person who created the automation has better personal access. This is what keeps a scheduled post from silently acting as a specific teammate.
</Step>
<Step title="Checks visibility">
Before posting, it re-confirms it can still see the destination channel.
</Step>
<Step title="Posts, or fails closed">
If anything above is unclear — a connection broke, visibility can't be verified — it skips that run rather than posting a guess. Silence beats a wrong digest.
</Step>
</Steps>
**Who can target what:**
| Destination | Who can create it | Reads from |
|---|---|---|
| Public channel | Any member | Org-shared connections, public channel memory |
| Private channel | Admins only | Org-shared connections, that channel's memory |
| DM to yourself | The owner of that DM | Your personal + org connections, your employee memory |
Common shapes worth stealing:
- A Monday-morning digest of open items and unanswered questions
- A daily Sentry error recap in `#eng`
- A weekly "what changed across our connected tools" summary
Anyone can create and manage their own automations; admins can manage everyone's. Ask Company Brain in Slack to set one up, or manage the full list from the web app.
## Proactiveness (chime-in)
Chime-in is different from an automation: there's no schedule, and no one asked. Company Brain is simply present in a channel — because an admin invited it — and it speaks up when staying quiet would waste someone's time.
**What actually earns a chime-in:**
- It has to add something the room doesn't already have — a fact, a correction, a next step — not agreement or a restatement of what's already visible.
- It has to come from somewhere it's genuinely allowed to look: [connected tools](/company-brain/connectors) or that room's own memory, same as any other answer.
- If it isn't confident the answer is actually correct, it says nothing. A wrong guess is worse than silence, so uncertainty resolves to silence, not a hedge.
<CodeGroup>
```text Worth chiming in
"is prod down? customers are pinging me"
→ correlates against Sentry, replies with what's actually elevated right now
```
```text Not worth it
"finally shipped this 🎉" (screenshot, no question)
→ stays quiet — there's nothing to add
```
</CodeGroup>
**Guardrails that keep it from becoming noise:**
- **Rate-limited.** It won't speak repeatedly in the same thread or channel in a short window, even if it technically could add something each time.
- **Invite-only rooms.** It never joins a channel on its own — only places an admin already invited it into.
- **Same graph as a normal answer.** A private channel's chime-in only ever draws on that channel's memory and public channel memory — never another private channel, never someone else's employee memory.
An explicit @mention always skips this judgment call entirely — naming it is you deciding it should speak, so it does.
<Note>
Automations and chime-in both write back to memory the same way a normal conversation does: a public channel's automation output lands in public channel memory, a private channel's chime-in stays scoped to that channel's memory.
</Note>
<CardGroup cols={2}>
<Card title="What you can do" icon="sparkles" href="/company-brain/use-cases/overview">
Real scenarios — support, incidents, digests, and more.
</Card>
<Card title="Connectors" icon="plug" href="/company-brain/connectors">
Wire up the tools automations and chime-in draw from.
</Card>
</CardGroup>

View file

@ -0,0 +1,64 @@
---
title: "Connectors"
sidebarTitle: "Connectors"
description: "Bring knowledge in with data connectors, and act in live tools with tool connectors"
icon: "plug"
---
Company Brain has two kinds of connectors. They look similar on the connections page, but they do different jobs:
| | Data connectors | Tool connectors |
|---|---|---|
| **What they do** | Bring knowledge *in* | Let the agent *act* in the tool |
| **Examples** | Google Drive, Notion, OneDrive | GitHub, Linear, Sentry, Plain, PostHog, Granola |
| **Result** | Docs land in public channel memory and stay searchable | Live reads and writes (list PRs, create issues, check errors) |
| **When it runs** | Background sync on a schedule | In the moment you ask |
## Data connectors
Data connectors sync existing files and docs into **public channel memory** so answers are grounded in real material — roadmaps, specs, handbooks, design docs.
How it works:
1. An admin connects a source (Drive, Notion workspace, OneDrive, and similar).
2. Company Brain fetches, chunks, embeds, and indexes the content in the background.
3. It re-syncs on a schedule automatically — you don't re-upload when a doc changes.
Connecting a data source is a **team-level action**. What comes in is visible org-wide, same as anything from a public channel — see the [permissions graph](/company-brain/permissions) for exactly who can read what.
<Note>
A data connector is only as useful as the docs you point it at. Start with the handful of sources people actually re-read — product specs, the handbook, the latest roadmap — rather than every folder in Drive.
</Note>
## Tool connectors
Tool connectors are live integrations (MCP-based under the hood). They don't just index past content — they read and act in the tool *right now*:
- **GitHub** — open PRs, recent commits, repo context
- **Linear** — find or create issues, check status
- **Sentry** — what's actually erroring in prod
- **Plain** — customer support tickets and history
- **PostHog** — product analytics
- **Granola** — meeting notes and decisions
- **Custom servers** — wire up your own MCP endpoint when the catalog doesn't cover a tool
You can also connect tools at two scopes — **Organization (shared)** or **Personal (yours)**. The full rule of thumb lives on [The permissions graph](/company-brain/permissions): reads prefer your personal connection and fall back to the org one; writes always run under your own account so the action is attributed to you.
If neither you nor the org has a tool connected, but a teammate does, Company Brain can ask them to **lease** temporary access for that one request — see [Leasing](/company-brain/permissions#leasing-borrowing-access-for-one-request).
## Which one do I need?
- **"What's in our Q2 roadmap?"** → data connector (Drive/Notion/OneDrive already synced)
- **"What are my open PRs?"** or **"Create a Linear issue"** → tool connector (GitHub / Linear)
- **"What did we decide in the Acme call?"** → tool connector that also brings knowledge in (Granola), or a data connector if notes live in Drive/Notion
You almost always want both: data connectors for the long-lived knowledge base, tool connectors for the live work happening this week.
<CardGroup cols={2}>
<Card title="Automations & proactiveness" icon="wand-magic-sparkles" href="/company-brain/automations">
Scheduled digests and unprompted replies that use these connections.
</Card>
<Card title="What you can do" icon="sparkles" href="/company-brain/use-cases/overview">
Walkthroughs of support, incidents, PRs, meetings, and more.
</Card>
</CardGroup>

View file

@ -0,0 +1,52 @@
---
title: "Using Outside Slack"
sidebarTitle: "Outside Slack"
description: "Reach the same permissions graph from Claude Code, ChatGPT, Cursor, or any MCP client"
icon: "globe"
---
Slack is the default surface, not the only one. Company Brain speaks MCP, so the same graph — your employee memory, the private channels you're in, public channel memory — is reachable from any MCP client: Claude Code, ChatGPT, Cursor, or anything else that speaks the protocol.
## Connect
Same endpoint as [Supermemory MCP](/supermemory-mcp/mcp) — there's no separate Company Brain server to point at:
```text
https://mcp.supermemory.ai/mcp
```
OAuth by default — your client discovers the authorization server and prompts you to sign in. Prefer an API key instead? Any key starting with `sm_` skips OAuth entirely.
<Note>
What changes isn't the URL, it's what shows up once you're connected. If your account belongs to an org with Company Brain, you get more than your own project spaces — your employee memory, the private channels you're in, and public channel memory all become available as workspaces, carrying your role and the exact same read/write access Slack already enforces.
</Note>
## Pick a workspace
Once connected, ask it what's available — it returns every container tag you have access to: your employee memory, each private channel memory you belong to, and public channel memory. Select one to make it the active workspace for the session; everything after that scopes to it automatically.
**Example:** from Claude Code, "what can I access in Acme's Company Brain?" surfaces your options as a picker — your employee memory, `#eng`'s private channel memory if you're in it, public channel memory. Pick one, and every search or save for the rest of the session happens inside it — the same as asking from that room in Slack.
## Tools
| Tool | What it does |
|---|---|
| `listContainerTags` | Everything you're allowed to read, with names and counts |
| `select-workspace` / `set-active-tag` | Pick which one is active for this session |
| `recall` | Search the active workspace, plus a profile summary when you're in your employee memory |
| `save-memory` | Write back to the active workspace |
| `memory-graph` | An interactive, visual map of a workspace's memories |
| `whoAmI` | Your role, access type, and active workspace — useful for sanity-checking what a client can actually see |
## Same graph, same guardrails
Nothing here is a side door. What you can reach follows the exact same [permissions graph](/company-brain/permissions) as Slack — an admin can restrict a member's connection to specific container tags the same way they'd scope a Slack channel invite, and every read or write is checked against that before it runs.
<CardGroup cols={2}>
<Card title="The permissions graph" icon="shield-check" href="/company-brain/permissions">
What each container tag actually is, and who can read it.
</Card>
<Card title="Supermemory MCP" icon="brain-circuit" href="/supermemory-mcp/mcp">
Base setup, auth, and personal project spaces on the same server.
</Card>
</CardGroup>

View file

@ -0,0 +1,99 @@
---
title: "What is Supermemory Company Brain?"
sidebarTitle: "Overview"
description: "A super agent, with all the knowledge and tools of your team"
icon: "brain"
---
import { SlackThread, SlackMessage, Mention, ChannelRef, AgentLink } from "/snippets/slack-message.mdx";
Supermemory Company Brain is a super agent with shared memory for your team that you can **ask questions** and that can **act in your tools**. It pulls from the work you already do - Slack threads, docs, GitHub, Linear - And can pretty much act as a real employee in your team.
<CardGroup cols={2}>
<Card title="Remembers" icon="brain">
Meetings, decisions, projects, and who owns what — kept current as your connected sources sync.
</Card>
<Card title="Acts" icon="bolt">
Pulls your open PRs, finds a Linear issue, or answers in a Slack channel — not just recites text back.
</Card>
</CardGroup>
## A quick example of its capabilities
It'll answer a question, actually go do the work, or just jump into a conversation on its own when it's got something worth saying — like a coworker would, not a search bar. You can configure how funny and unhinged it is, or how of it you want.
### Ask it anything
<SlackThread channel="#product">
<SlackMessage self hasAvatar time="10:42 AM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> what did we decide about pricing for the company brain
</SlackMessage>
<SlackMessage bot hasAvatar time="10:42 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
You and mahesh decided yesterday that it stays at **$400/mo**, usage based only. we landed on that in a <ChannelRef>product</ChannelRef> huddle
</SlackMessage>
</SlackThread>
### Put it to work
<SlackThread channel="#eng">
<SlackMessage self hasAvatar time="3:24 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> is the Stripe webhook timeout from Sentry already tracked anywhere?
</SlackMessage>
<SlackMessage bot hasAvatar time="3:25 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
nope, nothing yet — just opened **ENG-847** for it, added the sentry issue to it, and put it on <Mention>Sam</Mention> since they were the last one in `webhooks/stripe.ts` and their beautiful code broke it ☠️. <AgentLink href="https://linear.app">here you go</AgentLink>, should probably fix it asap.
</SlackMessage>
</SlackThread>
### Let it speak up on its own
<SlackThread channel="#eng" members={48}>
<SlackMessage name="Alex" color="#E01E5A" time="11:03 AM">
is prod down? a couple of customers are pinging me
</SlackMessage>
<SlackMessage bot hasAvatar time="11:03 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
not fully down — `api/search` is just elevated, 42 errors in the last 15 min (SM-2041), and <Mention>Kush</Mention> is on it. probably that deploy from this morning. Only one user has complained on support and i already replied to them saying it's being investigated.
</SlackMessage>
</SlackThread>
You don't need to mention it. It speaks up when it has something to add. It's smart and proactive!
## Same knowledge, useful everywhere
It's your team's knowledge — it doesn't have to stay in Slack. Take it wherever you're actually working:
- **Your coding agent** — ask Claude Code or Cursor mid-session what the team decided, why a file looks the way it does, or who to ping about it, without tabbing over to Slack.
- **Your own tools, via MCP** — Company Brain speaks MCP, so if whatever you're building can speak MCP too, it can ask. Plug it into an internal tool, a script, whatever you need.
Same permissions graph everywhere, no exceptions — asking from Claude Code doesn't get you anything asking from Slack wouldn't.
```text
> is the stripe webhook thing from earlier actually fixed?
yep — Sam shipped it in ENG-847 about an hour ago, Sentry's been quiet since
```
This knowledge can be used wherever you and your teammates go — see [Using outside Slack](/company-brain/outside-slack) for how to connect.
## Use it your way
Company Brain isn't locked to one model or one voice. Two things you control directly:
- **Any model, no markup** — bring your own LLM and pay nothing extra for inference.
- **Its tonality** — configure how it talks, from buttoned-up professional to fully unhinged. Make it sound like your team, not a generic chatbot.
## Where to go next
Company Brain has a handful of ideas worth understanding before you set it up: Our permissioning setup, how to configure it, proactiveness, automations, and more.
<CardGroup cols={2}>
<Card title="The permissions graph" icon="shield-check" href="/company-brain/permissions">
What's remembered where, and who can read it.
</Card>
<Card title="Setup and onboarding" icon="rocket" href="/company-brain/setup">
Get your team's workspace running.
</Card>
</CardGroup>

View file

@ -0,0 +1,91 @@
---
title: "The Permissions Graph"
sidebarTitle: "Permissions"
description: "What Company Brain remembers, who it's visible to, and how tool access is scoped"
icon: "shield-check"
---
Company Brain isn't split into "a shared brain" and "a private brain." It's a graph: memory is written to the narrowest room a conversation happened in, and what a given conversation can *read* depends on where it's happening and who's asking. Nothing here is silent — every install, channel read, and temporary access grant requires an explicit accept from a real person.
## Three memories, not two
<CardGroup cols={3}>
<Card title="Employee memory" icon="user">
One per person. Built from your DMs with the bot and what it learns about you over time. Only visible from your own DM.
</Card>
<Card title="Private channel memory" icon="lock">
One per private channel. Scoped to that room — visible to anyone in it, to no one outside it.
</Card>
<Card title="Public channel memory" icon="hash">
One per organization. Anything durable from a public channel lands here. The whole org can draw on it.
</Card>
</CardGroup>
A message writes to exactly one of these — whichever room it happened in.
## What a conversation can read
Writing is narrow; reading is broader, and it widens the more private the room is:
| Asking from | Can read |
|---|---|
| A public channel | Public channel memory |
| A private channel | That channel's memory + public channel memory |
| A DM with the bot | Your employee memory + public channel memory + every private channel memory you belong to |
```mermaid
flowchart LR
Pub["Public channel memory<br/>(the whole org)"]
Priv["Private channel memory<br/>(that room's members)"]
Emp["Employee memory<br/>(you, in DM)"]
Priv -.reads.-> Pub
Emp -.reads.-> Pub
Emp -.reads.-> Priv
```
A DM is the widest seat in the room precisely because it's the most private one — the bot answers you there with everything *you* could see, stitched together. A public channel is the opposite: the whole org can read it, so it only ever draws on what the whole org is allowed to know.
<Note>
If you're not in a private channel, its memory doesn't exist for you — not even by inference in a DM. The bot only ever reads with the asker's own access, so it can't surface something you couldn't otherwise see.
</Note>
**Example:** you DM the bot asking "what did we decide about the Acme deal?" It can draw on the public `#sales` channel, the private `#acme-deal` channel if you're in it, and anything it's learned about you directly — and it'll cite which one the answer came from. Ask the same question in `#general`, a public channel, and it can only answer from what `#general` and other public channels already know — the private `#acme-deal` context simply isn't in scope there.
## Tool access follows you, not the connection
Tools like GitHub and Linear can be connected two ways — **Organization (shared)**, set up once by an admin as a fallback the whole team can read from, or **Personal (yours)**, your own connection for your own reads and actions. Both show up on the same connections page; it's one tool catalog, connected at two possible scopes.
Whichever scope answered, the result is still bounded by what *you* could already see or do in that tool yourself — Company Brain never gets a standing key to "everything Linear knows." If you're not on a private Linear team, the bot can't surface those issues to you either, even through the org-shared connection.
| | Reads | Writes |
|---|---|---|
| **Behavior** | Try your personal connection first, then fall back to org-shared | Always run under your own connection |
| **Why** | Gives you the fullest access you're entitled to | Attributes the action to a real person, never a shared service account |
Admins can also act through the org-shared connection directly, for the cases where that's the point.
## Leasing: borrowing access for one request
Sometimes a request needs a tool neither you nor the org has connected — but a teammate has it connected personally. Rather than failing, Company Brain can ask that teammate directly: it posts a card in Slack asking them to approve or deny lending access for that one request.
- Nothing is granted silently — a real person has to accept the card.
- Access is short-lived and scoped to the single request that triggered it, not standing access to your account.
- The teammate can say no, and the request simply doesn't go through.
<Note>
Leasing is a fallback of last resort — it only comes up when nobody's connected the tool at the org level yet. See [Connectors](/company-brain/connectors) to close that gap for good.
</Note>
## API keys inherit the same graph
A scoped or agent API key can only reach what its owner could already reach by asking directly. A member can't mint a key that reads another member's employee memory or a private channel they're not in — the graph above applies identically whether a person is asking or a key is.
<CardGroup cols={2}>
<Card title="Connectors" icon="plug" href="/company-brain/connectors">
Set up the data and tool connections this page describes.
</Card>
<Card title="Automations & proactiveness" icon="wand-magic-sparkles" href="/company-brain/automations">
How scheduled runs and unprompted replies respect the same graph.
</Card>
</CardGroup>

View file

@ -0,0 +1,87 @@
---
title: "Setup and Onboarding"
sidebarTitle: "Setup"
description: "Creating a team workspace and installing it into Slack"
icon: "rocket"
---
Setting up Company Brain is two admin steps: create the workspace, then install it into Slack. Everyone else joins on their own after that — see [Greeting new teammates](/company-brain/use-cases/greeting).
## 1. Create your team workspace
Creating a workspace sets up your shared **Team Brain** and your private **My Brain** in one step.
<Steps>
<Step title="Sign up">
Head to [app.supermemory.ai](https://app.supermemory.ai) and create an account.
</Step>
<Step title="Choose Team">
On the **About** step, switch from **Personal** to **Team**.
<Note>
Team workspaces are invite-only during the private beta. Not invited yet? Email **support@supermemory.com**, or start Personal and invite your team once you're in.
</Note>
![Personal/Team toggle on sign-up, with the private-beta invite notice for Team](/images/company-brain/signup-team-toggle.png)
</Step>
<Step title="Add your company domain and confirm">
Enter your domain (for example `acme.com`) and confirm. Supermemory researches the company from there and seeds a starting profile, before any source finishes syncing.
![Company domain step — Supermemory researches the company from the domain to set up its Brain](/images/company-brain/signup-company-domain.png)
</Step>
<Step title="Add to Slack, connect apps, and invite your team">
All three run in parallel with research, and none of them block it:
- **Add to Slack** — kicks off the install flow below.
- **Connect apps** — Linear, Granola, Sentry, and more.
- **Invite teammates** — now, not later. No per-seat pricing, so invite everyone in your Slack.
![Research in progress, with Add to Slack and Connect apps available alongside it](/images/company-brain/signup-research-connect.png)
</Step>
<Step title="You're ready">
Supermemory's already learned a real amount about your company by the time research finishes. Watch Slack for a DM from it walking you through what it can do.
![The finished research — real notes about the company and founder, ready to search](/images/company-brain/signup-research-complete.png)
</Step>
</Steps>
<Note>
**Try it:** ask `What does {your company} do?` — you should get a real answer from the seeded profile.
</Note>
## 2. Install into Slack (admin)
<AccordionGroup>
<Accordion title="Don't have a Slack workspace yet?">
Go to [app.slack.com](https://app.slack.com) to create one first — Company Brain installs into an existing workspace, it doesn't create one for you.
![Naming a new Slack workspace](/images/company-brain/slack-create-workspace.png)
</Accordion>
</AccordionGroup>
<Steps>
<Step title="Confirm company and domain">
Click **Install to Slack**. Not an admin? This triggers Slack's own request-to-install flow instead.
</Step>
<Step title="Hand off to Slack">
The web app hands off immediately — "we've DM'd you in Slack."
</Step>
<Step title="Home channel">
The agent creates `#company-brain`, posts an intro there, and announces itself once in `#general`.
</Step>
<Step title="Approve channels to read">
You approve each channel with a tap — it never joins one silently.
</Step>
<Step title="Add your team">
Invite people with a picker, a workspace-wide toggle, or email.
</Step>
</Steps>
<CardGroup cols={2}>
<Card title="Greeting new teammates" icon="user-plus" href="/company-brain/use-cases/greeting">
What member join looks like from a new hire's side.
</Card>
<Card title="The permissions graph" icon="shield-check" href="/company-brain/permissions">
What each person can see once they're in.
</Card>
</CardGroup>

View file

@ -0,0 +1,58 @@
---
title: "Acting in Tools"
sidebarTitle: "Acting in Tools"
description: "List open PRs, create Linear issues — actions run under your own account"
icon: "wrench"
---
import { SlackThread, SlackMessage, Mention, AgentLink } from "/snippets/slack-message.mdx";
## The scenario
You don't just want to *know* something — you want Company Brain to do the next step in GitHub or Linear for you.
## What happens
### Read from a tool
<SlackThread channel="#eng">
<SlackMessage self hasAvatar time="3:22 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> what are my open PRs in the web repo?
</SlackMessage>
<SlackMessage bot hasAvatar time="3:22 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
You have **3 open PRs** on `supermemory/web`:
- `#1199` staff custom MCP connection cards — review requested
- `#1197` company brain onboarding research UI — changes requested
- `#1181` brain connections board — approved, not merged
</SlackMessage>
</SlackThread>
### Write to a tool
<SlackThread channel="#eng">
<SlackMessage self hasAvatar time="3:24 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> create a Linear issue: fix the onboarding redirect
</SlackMessage>
<SlackMessage bot hasAvatar time="3:24 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Created **ENG-842** — *Fix the onboarding redirect* in Engineering, assigned to you. <AgentLink href="https://linear.app">Open in Linear</AgentLink>
</SlackMessage>
</SlackThread>
## What's really going on
Both turns use [tool connectors](/company-brain/connectors) (GitHub, Linear). Reads try your **personal** connection first and fall back to the org-shared one. **Writes always run under your own account** — so the Linear issue is attributed to you, never silently as "the org."
If you haven't connected the tool and neither has the org, Company Brain can ask a teammate to [lease](/company-brain/permissions#leasing-borrowing-access-for-one-request) temporary access for that one request.
<CardGroup cols={2}>
<Card title="Permissions" icon="shield-check" href="/company-brain/permissions">
Personal vs org tools, and how leasing works.
</Card>
<Card title="Connectors" icon="plug" href="/company-brain/connectors">
Connect GitHub, Linear, and the rest.
</Card>
</CardGroup>

View file

@ -0,0 +1,53 @@
---
title: "Greeting New Teammates"
sidebarTitle: "Greeting Teammates"
description: "Connect card, welcome DM, and first answer — activation on day one"
icon: "user-plus"
---
import { SlackThread, SlackMessage } from "/snippets/slack-message.mdx";
## The scenario
A new hire joins the Slack workspace. They shouldn't need a web signup form or a long handbook read before Company Brain is useful — the whole first experience happens in Slack.
## What happens
They get a connect card, tap **Connect me**, and receive a welcome DM:
<SlackThread type="dm" dmWith={{ name: "supermemory" }} hasAvatar>
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
<SlackMessage bot hasAvatar time="9:02 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Welcome to **Acme**. Here's what I know, what I can access, and what I keep private.
Try one of these:
1. What does Acme do?
2. Who owns onboarding?
3. Where do we track bugs?
</SlackMessage>
<SlackMessage name="Sam" color="#36C5F0" time="9:03 AM">
What does Acme do?
</SlackMessage>
<SlackMessage bot hasAvatar time="9:03 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Acme builds memory infrastructure for AI apps — shared context for teams and agents. *(from the company profile your admin seeded at setup)*
</SlackMessage>
</SlackThread>
Right after the first answer, they're prompted to connect personal tools (Linear, Notion) so day-two questions can hit live data.
## What's really going on
This is the [member join flow](/company-brain/setup) told as a scenario. Tapping **Connect me** creates a passwordless account from their Slack email, provisions their private [employee memory](/company-brain/permissions), and consumes a seat. The three starter questions are seeded so the first useful answer happens on the first tap — that's the activation moment.
No web app required for the new hire. Admins still do the one-time workspace + Slack install on the web.
<CardGroup cols={2}>
<Card title="Setup and onboarding" icon="rocket" href="/company-brain/setup">
The full admin + member flow.
</Card>
<Card title="Permissions" icon="shield-check" href="/company-brain/permissions">
Employee memory vs. public and private channel memory.
</Card>
</CardGroup>

View file

@ -0,0 +1,48 @@
---
title: "Incidents & Downtime Chatter"
sidebarTitle: "Incidents"
description: "Answer 'is prod down?' from live Sentry, and post scheduled error digests"
icon: "triangle-alert"
---
import { SlackThread, SlackMessage, AgentLink } from "/snippets/slack-message.mdx";
## The scenario
Someone drops a vague "is prod down?" in `#eng`. Company Brain correlates it with what's actually failing right now — and a separate automation keeps a daily error digest flowing without anyone asking.
## What happens
<SlackThread channel="#eng" members={48}>
<SlackMessage name="Alex" color="#E01E5A" time="11:03 AM">
is prod down? a couple of customers are pinging me
</SlackMessage>
<SlackMessage bot hasAvatar time="11:03 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Not a full outage — **`api/search` is elevated** (42 errors in the last 15m, mostly `timeout on vectorize`). Sentry issue <AgentLink href="https://example.com">SM-2041</AgentLink>. Deploy from this morning is the likely culprit; rollback candidate is `af61880`.
</SlackMessage>
</SlackThread>
Or, without anyone asking, a scheduled automation posts every morning:
<SlackThread channel="#eng">
<SlackMessage bot hasAvatar time="9:00 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
**Daily error digest** — 3 issues new since yesterday, 1 still spiking (`api/search` timeout). Full list in Sentry.
</SlackMessage>
</SlackThread>
## What's really going on
The first exchange is a [chime-in](/company-brain/automations) (or an @mention — either works) that hits the **Sentry** [tool connector](/company-brain/connectors) for live error data. The second is an [automation](/company-brain/automations): a recurring prompt on a cron that posts to a channel using org-shared connections only, so it never runs as a specific person.
If the channel is private, only an admin can target it with an automation, and the bot fails closed if it can't verify visibility.
<CardGroup cols={2}>
<Card title="Connectors" icon="plug" href="/company-brain/connectors">
Connect Sentry (and friends) for live incident context.
</Card>
<Card title="Automations" icon="wand-magic-sparkles" href="/company-brain/automations">
Schedule digests and understand chime-in.
</Card>
</CardGroup>

View file

@ -0,0 +1,45 @@
---
title: "Answering from Your Docs"
sidebarTitle: "From Docs"
description: "Ask about roadmaps, specs, and handbooks in plain language"
icon: "book"
---
import { SlackThread, SlackMessage, Mention } from "/snippets/slack-message.mdx";
## The scenario
The Q2 roadmap lives in Notion. Nobody wants to dig for the link mid-conversation — they just ask.
## What happens
<SlackThread channel="#product">
<SlackMessage self hasAvatar time="10:18 AM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> what's in our Q2 roadmap?
</SlackMessage>
<SlackMessage bot hasAvatar time="10:18 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Three themes for Q2:
1. **Company Brain GA** — Slack bot, connectors, automations
2. **Memory Router v2** — lower latency, better multi-tenant isolation
3. **Enterprise SSO + audit log**
Full doc: *Q2 Roadmap* in Notion (last updated Jun 3).
</SlackMessage>
</SlackThread>
## What's really going on
This is a [data connector](/company-brain/connectors) (Notion, Google Drive, or OneDrive) feeding [public channel memory](/company-brain/permissions). Content is fetched, chunked, embedded, and re-synced on a schedule — so when the roadmap changes in Notion, answers stay current without anyone re-uploading.
Same pattern works for handbooks, design docs, RFCs, and "where do we document X?" style questions. If it doesn't know, it says so rather than guessing.
<CardGroup cols={2}>
<Card title="Connectors" icon="plug" href="/company-brain/connectors">
Bring Drive, Notion, and OneDrive into public channel memory.
</Card>
<Card title="Permissions" icon="shield-check" href="/company-brain/permissions">
Who can see what once it's indexed.
</Card>
</CardGroup>

View file

@ -0,0 +1,49 @@
---
title: "Long-Horizon Research"
sidebarTitle: "Long-Horizon Research"
description: "Questions that need many sources synthesized into one answer"
icon: "telescope"
---
import { SlackThread, SlackMessage, Mention } from "/snippets/slack-message.mdx";
## The scenario
Some questions aren't a quick lookup. They need pulling information from a lot of distinct places — docs, tickets, meetings, code, past Slack threads — and synthesizing it into one coherent answer. Closer to a research project than "what's the pricing?"
## What it will look like
<SlackThread channel="#product">
<SlackMessage self hasAvatar time="11:10 AM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> prepare a brief on how we've handled enterprise SSO requests over the last two quarters — who asked, what we promised, what's still open, and what engineering thinks the real effort is
</SlackMessage>
<SlackMessage bot hasAvatar time="11:14 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
**Enterprise SSO — two-quarter brief**
- **Demand:** 6 serious asks (Acme, Globex, Initech, …). Three converted to paid pilots.
- **Promises made:** Okta + Azure AD for pilots; custom SAML called "roadmap" in two decks.
- **Still open:** Globex blocked on SCIM; Initech waiting on audit-log export.
- **Eng view:** ~6 weeks for Okta/Azure path (from `#eng` thread Mar 18 + Linear ENG-610); SCIM is the long pole.
Sources: Q1/Q2 enterprise notes, Linear ENG-610, Granola "SSO planning", `#sales` threads. Want this as a Linear doc?
</SlackMessage>
</SlackThread>
## What's really going on
Today, a single turn can already hit multiple [connectors](/company-brain/connectors) and channel memory in one answer. Long-horizon research is for the cases where that isn't enough — the work needs more time, more parallel digging, and a structured synthesis step before it replies.
We're deliberately not documenting an internal "spawn N children" mechanism here. What matters for you: ask a research-shaped question, get a sourced brief back, without you having to run six separate searches.
Until it ships, break big questions into smaller ones (docs, then tickets, then "summarize what we have") — Company Brain already handles each of those well.
<CardGroup cols={2}>
<Card title="From docs" icon="book" href="/company-brain/use-cases/knowledge-recall">
What single-turn knowledge recall looks like today.
</Card>
<Card title="Meeting recall" icon="calendar-check" href="/company-brain/use-cases/meeting-recall">
Pulling decisions out of synced notes.
</Card>
</CardGroup>

View file

@ -0,0 +1,40 @@
---
title: "Meeting Recall"
sidebarTitle: "Meeting Recall"
description: "Ask what was decided in a meeting and get the answer from synced notes"
icon: "calendar-check"
---
import { SlackThread, SlackMessage, Mention } from "/snippets/slack-message.mdx";
## The scenario
Someone missed the Acme call — or was there and forgot the outcome. They ask in Slack and get the decision, with a path back to the source notes.
## What happens
<SlackThread channel="#sales">
<SlackMessage self hasAvatar time="4:05 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> what did we decide in the meeting with Acme?
</SlackMessage>
<SlackMessage bot hasAvatar time="4:05 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Acme is moving to annual billing at the current rate, with a 90-day pilot on the enterprise SSO add-on. Follow-up owned by **Priya**, due Friday. *(from Granola notes, Acme QBR — Jun 11)*
</SlackMessage>
</SlackThread>
## What's really going on
Meeting notes come in through a [connector](/company-brain/connectors) — Granola for live meeting notes, or Drive/Notion if notes land there. Once synced, they live in [public channel memory](/company-brain/permissions), so anyone on the team can ask about them. The answer cites the source so you can open the original notes and verify.
Ask about a decision, an owner, a date, or "what did we promise them" — plain language is fine.
<CardGroup cols={2}>
<Card title="Permissions" icon="shield-check" href="/company-brain/permissions">
Why meeting notes land in public channel memory.
</Card>
<Card title="Connectors" icon="plug" href="/company-brain/connectors">
Connect Granola, Drive, or Notion.
</Card>
</CardGroup>

View file

@ -0,0 +1,43 @@
---
title: "Meeting Scheduling"
sidebarTitle: "Scheduling"
description: "Find free time and send a calendar invite from Slack"
icon: "calendar-plus"
---
import { SlackThread, SlackMessage, Mention } from "/snippets/slack-message.mdx";
<Note>
**Coming soon.** Calendar scheduling isn't in Company Brain yet — there's no calendar connector in the catalog today. This page shows the experience we're building toward, using a real exchange from our own team.
</Note>
## The scenario
A teammate needs help and doesn't want to play calendar ping-pong. They ask in Slack; Company Brain checks availability and offers to book the slot.
## What it will look like
<SlackThread channel="#eng">
<SlackMessage name="Sam" color="#36C5F0" time="1:12 PM">
Dhravya are you free this afternoon? need some help with dev setup
</SlackMessage>
<SlackMessage bot hasAvatar time="1:12 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Dhravya is free at **2:00 PM**. Sending an invite to block **20 mins** on both your calendars.
</SlackMessage>
</SlackThread>
## What's really going on
When this ships, it will be a [tool connector](/company-brain/connectors) against the calendar — same personal-vs-org rules and [write-under-your-account](/company-brain/permissions) attribution as Linear or GitHub. Creating an invite is a write, so it runs as the person who has the calendar connected (or via an explicit [lease](/company-brain/permissions#leasing-borrowing-access-for-one-request) if someone else is lending access for that one request).
Until then: ask Company Brain for *context* around scheduling ("who's the right person for dev setup?" / "when did we last pair on this?") and book the time the usual way.
<CardGroup cols={2}>
<Card title="Permissions" icon="shield-check" href="/company-brain/permissions">
How personal tools and leasing will apply to calendar.
</Card>
<Card title="What you can do" icon="sparkles" href="/company-brain/use-cases/overview">
Back to all scenarios — including what's shipped today.
</Card>
</CardGroup>

View file

@ -0,0 +1,47 @@
---
title: "What You Can Do"
sidebarTitle: "Overview"
description: "Real scenarios for Company Brain — from Slack answers to sandbox debugging"
icon: "sparkles"
---
Company Brain is most useful when it shows up in the work you already do. These walkthroughs are short, concrete scenarios — each one is a real exchange, what the bot is actually doing under the hood, and which concept page to read if you want the full picture.
## Shipped today
<CardGroup cols={2}>
<Card title="Automatic support" icon="headset" href="/company-brain/use-cases/support">
Customer question in Slack; Company Brain chimes in with the answer.
</Card>
<Card title="Incidents & downtime" icon="triangle-alert" href="/company-brain/use-cases/incidents">
"Is prod down?" answered from live Sentry, plus scheduled digests.
</Card>
<Card title="Meeting recall" icon="calendar-check" href="/company-brain/use-cases/meeting-recall">
"What did we decide with Acme?" from synced meeting notes.
</Card>
<Card title="Answering from docs" icon="book" href="/company-brain/use-cases/knowledge-recall">
Roadmaps, specs, and handbooks — asked in plain language.
</Card>
<Card title="Acting in tools" icon="wrench" href="/company-brain/use-cases/acting-in-tools">
List open PRs, create a Linear issue — under your own account.
</Card>
<Card title="Greeting new teammates" icon="user-plus" href="/company-brain/use-cases/greeting">
Connect card, welcome DM, first answer — activation on day one.
</Card>
<Card title="Sandbox debugging" icon="terminal" href="/company-brain/use-cases/sandbox-debugging">
Actually run the failing test in an isolated workspace.
</Card>
</CardGroup>
## Coming soon
<CardGroup cols={2}>
<Card title="Long-horizon research" icon="telescope" href="/company-brain/use-cases/long-horizon-research">
Questions that need many sources synthesized into one answer.
</Card>
<Card title="Meeting scheduling" icon="calendar-plus" href="/company-brain/use-cases/meeting-scheduling">
Find free time and send a calendar invite from Slack.
</Card>
</CardGroup>
If you're still setting things up, start with [What is Company Brain?](/company-brain/overview) and work through the Concepts section first — the walkthroughs assume those ideas.

View file

@ -0,0 +1,51 @@
---
title: "Sandbox Debugging"
sidebarTitle: "Sandbox"
description: "Hand it a failing test — it runs the code in an isolated workspace and reports back"
icon: "terminal"
---
import { SlackThread, SlackMessage, Mention } from "/snippets/slack-message.mdx";
## The scenario
A test is failing and the stack trace alone isn't enough. You want Company Brain to actually run the code, not just describe what it *would* do.
## What happens
<SlackThread channel="#eng">
<SlackMessage self hasAvatar time="5:41 PM">
<img src="/images/company-brain/dhravya-slack-icon.jpg" alt="" />
<Mention>supermemory</Mention> the `permissions.lease` unit test is failing on main — can you reproduce and tell me why?
</SlackMessage>
<SlackMessage bot hasAvatar time="5:42 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Reproduced in a sandbox against `main`.
**Failure:** `permissions.lease` expects temporary access to expire in 15 minutes, but the fixture still asserts 30.
**Cause:** the default was shortened in `#1188`; the unit test wasn't updated.
**Fix:** point the assertion at the shared default so it can't drift again.
</SlackMessage>
</SlackThread>
## What's really going on
Company Brain spins up an **isolated workspace**, checks out what it needs, runs the command, and reads the output. It's not guessing from static code alone — it's executing.
Guardrails are intentional, so it's safe to hand real tasks to:
- No `git push`, no deploys
- No elevated privileges (`sudo` and similar are blocked)
- No reaching arbitrary internal network addresses
- No long-running dev servers
Think of it as a sealed workbench: useful for reproduce / inspect / explain loops, not for shipping changes on your behalf.
<CardGroup cols={2}>
<Card title="Acting in tools" icon="wrench" href="/company-brain/use-cases/acting-in-tools">
When the next step is a Linear issue or a PR lookup.
</Card>
<Card title="What you can do" icon="sparkles" href="/company-brain/use-cases/overview">
All the scenario walkthroughs.
</Card>
</CardGroup>

View file

@ -0,0 +1,101 @@
---
title: "From Support Ticket to Code Fix"
sidebarTitle: "Escalation"
description: "A Plain ticket gets triaged in Slack, and an @mention hands the fix to Cursor"
icon: "bug"
---
import { SlackThread, SlackMessage, Mention, FileAttachment, AgentLink, SlackUnfurl, SlackButton } from "/snippets/slack-message.mdx";
## The scenario
A customer files a ticket through Plain. It lands in `#support`, gets triaged with context Company Brain already has lying around, and — instead of someone manually filing a bug and waiting — an @mention hands the whole thing straight to Cursor.
## What happens
<SlackThread channel="#support" members={24}>
<SlackMessage
name="Plain"
badges={["APP"]}
hasAvatar
time="10:12 AM"
>
<img src="/images/company-brain/plain-icon.png" alt="" />
New conversation: <AgentLink href="#">rewriteQuery param not working</AgentLink>
<br />
**Jordan Alvarez** (acme-corp.io) sent a **new message**.
<SlackUnfurl footer="Added by Plain">
hi team, just tried the `rewriteQuery` param on the v3 search endpoint and it doesn't seem to actually do anything — tried a few different values, results look identical either way. can someone take a look
</SlackUnfurl>
</SlackMessage>
<SlackMessage bot hasAvatar time="10:13 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Confirmed, this is a real one — a couple of people have also flagged it on GitHub over the last week.
<br />
<br />
Quick context: v3 search is deprecated, but we've committed to legacy support through end of year, so it's still worth fixing rather than telling people to migrate. Most likely cause is a change <Mention>Adam</Mention> shipped last week to cut down query-rewrite costs — looks like it short-circuits before `rewriteQuery` gets applied in some cases.
<br />
<br />
<Mention>cursor</Mention> can you take this one? Full context attached.
<br />
<br />
<FileAttachment name="Context.md" />
</SlackMessage>
<SlackMessage
name="Cursor"
badges={["AGENT"]}
hasAvatar
time="10:14 AM"
>
<img src="/images/company-brain/cursor-icon.png" alt="" />
<AgentLink href="#">Agent thread started</AgentLink>
<br />
Reproducing against the v3 search test suite now.
</SlackMessage>
<SlackMessage
name="Cursor"
badges={["AGENT"]}
hasAvatar
time="10:19 AM"
>
<img src="/images/company-brain/cursor-icon.png" alt="" />
Fixed — `rewriteQuery` was getting skipped by the new cost short-circuit whenever a query was already cached. Pushed on <AgentLink href="#">#2312</AgentLink>.
<br />
<br />
**Resolution:**
<br />
• Scoped the short-circuit to skip only the rewrite step, not the whole `rewriteQuery` path
<br />
• Added a regression test covering `rewriteQuery` against a cache hit
<br />
<br />
**Repository:** `supermemoryai/mono`
<br />
<br />
<SlackButton variant="primary">Open in Web</SlackButton>
<SlackButton>Open in Desktop</SlackButton>
</SlackMessage>
<SlackMessage bot hasAvatar time="10:20 AM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
<Mention self>Dhravya</Mention> I'll let you review that and let the customer know we have a fix ready.
<br />
Please do it ASAP — it's an enterprise customer!
</SlackMessage>
</SlackThread>
## What's really going on
The first two turns are the same pattern as [Automatic Support](/company-brain/use-cases/support): the bot is already a member of `#support`, so it [chimes in](/company-brain/automations) unprompted, correlating the ticket against known GitHub issues and whatever it knows about the codebase and the v3 deprecation timeline.
The handoff to Cursor is different. That's not a chime-in — it's an explicit `@mention`, and Cursor is wired in as a [tool connector](/company-brain/connectors) (a custom MCP server, same as GitHub or Linear under the hood) that can act, not just answer. Naming it by name is what triggers the write: Company Brain hands off the attached context and Cursor opens its own agent thread against the repo, the same way a mention of GitHub or Linear in [Acting in Tools](/company-brain/use-cases/acting-in-tools) triggers a write rather than a read. Nothing happens in the codebase without that explicit ask.
Whether that handoff is even possible follows the same [permissions](/company-brain/permissions) rules as any other tool: it runs under whichever connection — personal or org-shared — is actually wired up for Cursor, and it's scoped to what that connection can see.
<CardGroup cols={2}>
<Card title="Acting in Tools" icon="wrench" href="/company-brain/use-cases/acting-in-tools">
How @mentions trigger writes instead of reads.
</Card>
<Card title="Connectors" icon="plug" href="/company-brain/connectors">
Wire up Plain, GitHub, and custom MCP servers like Cursor.
</Card>
</CardGroup>

View file

@ -0,0 +1,39 @@
---
title: "Automatic Support"
sidebarTitle: "Support"
description: "Company Brain chimes in on customer questions with answers from docs and tickets"
icon: "headset"
---
import { SlackThread, SlackMessage } from "/snippets/slack-message.mdx";
## The scenario
A customer question lands in `#support`. Nobody has to @mention the bot — it already has the answer from past tickets and the help docs.
## What happens
<SlackThread channel="#support" members={24}>
<SlackMessage name="Maya" color="#2BAC76" time="2:14 PM">
customer on the Pro plan is asking if they can export their full memory graph as CSV — do we support that?
</SlackMessage>
<SlackMessage bot hasAvatar time="2:14 PM">
<img src="/images/company-brain/supermemory-slack-icon.png" alt="" />
Yes — **Settings → Export → Full graph (CSV)**. Available on Pro and above. Same answer went out on ticket PLN-1842 last week if you want the exact wording.
</SlackMessage>
</SlackThread>
## What's really going on
This is [proactiveness (chime-in)](/company-brain/automations) plus a connected support tool (Plain) and public channel memory. The bot is already a member of `#support` (an admin invited it — it never joins on its own). It decided the answer was clear enough to speak without being asked, pulled the export path from docs in public channel memory, and cited a recent ticket from Plain.
Same channel scope rules apply: a public support channel writes durable learnings back to public channel memory; a private support channel keeps them scoped to that room's own memory. See [Permissions](/company-brain/permissions).
<CardGroup cols={2}>
<Card title="Automations & proactiveness" icon="wand-magic-sparkles" href="/company-brain/automations">
How chime-in decides when to speak.
</Card>
<Card title="Connectors" icon="plug" href="/company-brain/connectors">
Wire up Plain and your help docs.
</Card>
</CardGroup>

View file

@ -0,0 +1,179 @@
---
title: "Container Tags"
sidebarTitle: "Container tags"
description: "The isolation boundary that groups and partitions memories by user, project, or any logical scope"
icon: "folder"
---
A **container tag** is the primary way you organize and isolate memories in Supermemory. It's a simple string identifier you attach to content when you add it — and that you pass back when you search, list, or update it.
Think of a container tag as a **namespace**: every memory tagged with `user_alex` lives in its own isolated space, completely separate from memories tagged `user_jordan`. This is what makes Supermemory safe to use in multi-tenant applications — one user can never see another user's memories unless you explicitly query across both tags.
<CardGroup cols={2}>
<Card title="Group" icon="layers">
Bucket memories by user, project, agent, workspace, or any boundary that makes sense for your app.
</Card>
<Card title="Isolate" icon="shield">
Each container tag maps to its own vector namespace, so search and retrieval never leak across boundaries.
</Card>
</CardGroup>
---
## How it works
When you add a memory with a container tag, Supermemory automatically creates a **space** for that tag (scoped to your organization) if one doesn't already exist. You don't need to provision anything ahead of time — the first write with a new tag creates the container, and subsequent writes reuse it.
```typescript
// First call auto-creates the "user_alex" container
await client.add({
content: "Alex prefers dark mode and concise answers",
containerTag: "user_alex",
});
// Later, retrieve only Alex's memories
const results = await client.search({
q: "what are the user's UI preferences?",
containerTag: "user_alex",
});
```
Under the hood, each container tag is hashed into a dedicated vector namespace. Embeddings, chunks, and memory entries for one tag are stored and searched independently of every other tag — there is no shared index to filter through, which is why isolation is strict rather than best-effort.
<Note>
A container tag is an **opaque identifier you choose**. Supermemory does not parse meaning out of it — `user_123`, `project_mobile`, and `org:acme:team:growth` are all equally valid. Pick a convention that mirrors the access boundaries in your own application.
</Note>
---
## Naming rules
Container tags are validated on every request. A tag must:
- Be **100 characters or less**
- Contain only **alphanumeric characters, hyphens (`-`), underscores (`_`), and colons (`:`)**
Matching pattern: `^[a-zA-Z0-9_:-]+$`
```typescript
// ✅ Valid
"user_123"
"project-mobile-app"
"org:acme:user:john"
"tenant_42_workspace_7"
// ❌ Invalid — spaces, slashes, and other symbols are rejected
"user 123"
"project/mobile"
"team@acme"
```
The colon is intentionally allowed so you can build **hierarchical** tags (for example `org:acme:user:john`) that encode several levels of structure in a single identifier.
---
## `containerTag` vs `containerTags`
Supermemory's current API uses a **single** `containerTag` string per request.
<Warning>
The plural `containerTags` array field is **deprecated**. It still works for backward compatibility on older (`/v3`) endpoints, but new integrations should use the singular `containerTag` string. The `/v4` API only accepts `containerTag`.
</Warning>
| API field | Type | Status |
|-----------|------|--------|
| `containerTag` | `string` | ✅ Current — use this |
| `containerTags` | `string[]` | ⚠️ Deprecated |
---
## Where container tags are used
The same tag flows through the entire lifecycle of a memory. Pass it consistently and your data stays neatly partitioned.
| Operation | Behavior |
|-----------|----------|
| **Add** | Writes the memory into the tag's container (auto-creating the space). |
| **Search** | Restricts retrieval to the given tag's namespace. |
| **List** | Returns only memories belonging to the tag(s). |
| **Update / Delete** | Targets the memory inside the specified tag's container. |
```typescript
// Add
await client.add({ content: "Q1 planning notes", containerTag: "project_q1" });
// Search within the same container
await client.search({ q: "planning", containerTag: "project_q1" });
// List everything in the container
await client.documents.list({ containerTags: ["project_q1"] });
```
---
## Access control
Container tags are also an **authorization boundary**, not just an organizational one. Two mechanisms can restrict which tags a given caller may touch:
- **API key scopes** — an API key can be limited to a specific set of container tags, with read or write permission per tag.
- **Member restrictions** — an organization member can be granted access to only certain container tags.
When a request is restricted, Supermemory validates the requested tag against the caller's allowed set:
- Requesting a tag outside the allowed set returns `403 Forbidden`.
- A write (add/update/delete) to a read-only tag returns `403 Forbidden`.
- If no tag is supplied by a restricted caller, the request is automatically scoped to their allowed tag(s).
This means you can hand out an API key that is physically incapable of reading or writing another tenant's data, enforced at the data layer rather than in your application code.
---
## Per-container settings
Each container tag can carry its own configuration, independent of other tags in the same organization:
| Setting | Purpose |
|---------|---------|
| `name` | A human-friendly display name for the container. |
| `entityContext` | A custom context prompt applied when processing documents in this container — useful for steering extraction and summarization per project or tenant. |
```typescript
await client.containerTags.update("project_research", {
entityContext: "This project contains research papers about machine learning.",
});
```
Container tags can also be **merged** when you need to consolidate two buckets of memories into one.
---
## Choosing a convention
Pick a tagging scheme that maps onto the isolation boundaries your application actually needs.
| Pattern | Example | Use case |
|---------|---------|----------|
| User isolation | `user_{userId}` | Per-user memory in a consumer app |
| Project grouping | `project_{projectId}` | Project- or workspace-scoped content |
| Agent scoping | `agent_{agentId}` | Separate long-term memory per AI agent |
| Hierarchical | `org:{orgId}:user:{userId}` | Multi-level, multi-tenant SaaS |
<Tip>
Keep tags **deterministic** — derive them directly from IDs you already have (a user ID, a tenant ID) so you can always reconstruct the right tag at query time without a lookup.
</Tip>
---
## Next steps
<CardGroup cols={2}>
<Card title="Organizing & Filtering" icon="filter" href="/concepts/filtering">
Combine container tags with metadata filters for precise retrieval.
</Card>
<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.
</Card>
</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">

View file

@ -1,8 +1,8 @@
--- ---
title: "Organizing & Filtering Memories" title: "Organizing & Filtering Memories"
sidebarTitle: "Multi-Tenancy / Filtering" sidebarTitle: "Metadata filtering"
description: "Use container tags and metadata to organize and retrieve memories" description: "Use container tags and metadata to organize and retrieve memories"
icon: "users" icon: "filter"
--- ---
Supermemory provides two ways to organize your memories: Supermemory provides two ways to organize your memories:
@ -29,21 +29,22 @@ Container tags create isolated memory spaces. Use them to separate memories by u
```typescript ```typescript
await client.add({ await client.add({
content: "Meeting notes from Q1 planning", content: "Meeting notes from Q1 planning",
containerTags: ["user_123"] containerTag: "user_123"
}); });
``` ```
### Searching with Tags ### Searching with Tags
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "planning notes", q: "planning notes",
containerTags: ["user_123"] containerTag: "user_123",
searchMode: "documents"
}); });
``` ```
<Note> <Note>
Container tags use **exact array matching**. A memory tagged `["user_123", "project_a"]` won't match a search for just `["user_123"]`. Each search is scoped to a single container tag. Passing `containerTag: "user_123"` restricts results to memories stored in that container.
</Note> </Note>
### Recommended Patterns ### Recommended Patterns
@ -60,34 +61,34 @@ Container tags use **exact array matching**. A memory tagged `["user_123", "proj
// Multi-tenant SaaS - isolate by organization and user // Multi-tenant SaaS - isolate by organization and user
await client.add({ await client.add({
content: "Company policy document", content: "Company policy document",
containerTags: ["org_acme_user_john"] containerTag: "org_acme_user_john"
}); });
// Search only within that user's org context // Search only within that user's org context
const results = await client.search.documents({ const results = await client.search({
q: "vacation policy", q: "vacation policy",
containerTags: ["org_acme_user_john"] containerTag: "org_acme_user_john",
searchMode: "documents"
}); });
// Project-based isolation // Project-based isolation
await client.add({ await client.add({
content: "Sprint 5 retrospective notes", content: "Sprint 5 retrospective notes",
containerTags: ["project_mobile_app"] containerTag: "project_mobile_app"
}); });
// Time-based segmentation // Time-based segmentation
await client.add({ await client.add({
content: "Q1 2024 financial report", content: "Q1 2024 financial report",
containerTags: ["user_cfo_2024_q1"] containerTag: "user_cfo_2024_q1"
}); });
``` ```
**API field differences:** **API field differences:**
| Endpoint | Field | Type | | Operation | Field | Type |
|----------|-------|------| |-----------|-------|------|
| `/v3/search` | `containerTags` | Array | | Search | `containerTag` | String |
| `/v4/search` | `containerTag` | String | | Documents list | `containerTags` | Array |
| `/v3/documents/list` | `containerTags` | Array |
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>
@ -102,7 +103,7 @@ Metadata lets you attach custom properties to memories and filter by them later.
```typescript ```typescript
await client.add({ await client.add({
content: "Technical design document for auth system", content: "Technical design document for auth system",
containerTags: ["user_123"], containerTag: "user_123",
metadata: { metadata: {
category: "engineering", category: "engineering",
priority: "high", priority: "high",
@ -116,9 +117,10 @@ await client.add({
Filters must be wrapped in `AND` or `OR` arrays: Filters must be wrapped in `AND` or `OR` arrays:
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "design document", q: "design document",
containerTags: ["user_123"], containerTag: "user_123",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ key: "category", value: "engineering" }, { key: "category", value: "engineering" },
@ -142,8 +144,9 @@ const results = await client.search.documents({
Use `AND` and `OR` for complex queries: Use `AND` and `OR` for complex queries:
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "meeting notes", q: "meeting notes",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ key: "type", value: "meeting" }, { key: "type", value: "meeting" },
@ -163,8 +166,9 @@ const results = await client.search.documents({
Use `negate: true` to exclude matches: Use `negate: true` to exclude matches:
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "documentation", q: "documentation",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ key: "status", value: "draft", negate: true } { key: "status", value: "draft", negate: true }
@ -178,8 +182,9 @@ const results = await client.search.documents({
**String contains (substring search):** **String contains (substring search):**
```typescript ```typescript
// Find documents with "machine learning" in the description // Find documents with "machine learning" in the description
const results = await client.search.documents({ const results = await client.search({
q: "AI research", q: "AI research",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ {
@ -196,8 +201,9 @@ const results = await client.search.documents({
**Numeric comparisons:** **Numeric comparisons:**
```typescript ```typescript
// Find high-priority items created after a specific date // Find high-priority items created after a specific date
const results = await client.search.documents({ const results = await client.search({
q: "tasks", q: "tasks",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ {
@ -220,8 +226,9 @@ const results = await client.search.documents({
**Array contains (check array membership):** **Array contains (check array membership):**
```typescript ```typescript
// Find documents where a specific user is a participant // Find documents where a specific user is a participant
const results = await client.search.documents({ const results = await client.search({
q: "meeting notes", q: "meeting notes",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ {
@ -237,8 +244,9 @@ const results = await client.search.documents({
**Complex nested filters:** **Complex nested filters:**
```typescript ```typescript
// (category = "tech" OR category = "science") AND status != "archived" // (category = "tech" OR category = "science") AND status != "archived"
const results = await client.search.documents({ const results = await client.search({
q: "research papers", q: "research papers",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ {
@ -265,9 +273,10 @@ const results = await client.search.documents({
<Accordion title="Real-World Patterns"> <Accordion title="Real-World Patterns">
**User's work documents from 2024:** **User's work documents from 2024:**
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "quarterly report", q: "quarterly report",
containerTags: ["user_123"], containerTag: "user_123",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ key: "category", value: "work" }, { key: "category", value: "work" },
@ -280,9 +289,10 @@ const results = await client.search.documents({
**Team meeting notes with specific participants:** **Team meeting notes with specific participants:**
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "sprint planning", q: "sprint planning",
containerTags: ["project_alpha"], containerTag: "project_alpha",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ key: "type", value: "meeting" }, { key: "type", value: "meeting" },
@ -299,8 +309,9 @@ const results = await client.search.documents({
**Exclude drafts and deprecated content:** **Exclude drafts and deprecated content:**
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "documentation", q: "documentation",
searchMode: "documents",
filters: { filters: {
AND: [ AND: [
{ key: "status", value: "draft", negate: true }, { key: "status", value: "draft", negate: true },
@ -322,7 +333,7 @@ const results = await client.search.documents({
```typescript ```typescript
await client.add({ await client.add({
content: "Your content here", content: "Your content here",
containerTags: ["user_123"], // Isolation containerTag: "user_123", // Isolation
metadata: { key: "value" } // Custom properties metadata: { key: "value" } // Custom properties
}); });
``` ```
@ -330,9 +341,10 @@ await client.add({
### When Searching ### When Searching
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "search query", q: "search query",
containerTags: ["user_123"], // Must match exactly containerTag: "user_123", // Scopes results to this container
searchMode: "documents",
filters: { // Optional metadata filters filters: { // Optional metadata filters
AND: [{ key: "status", value: "published" }] AND: [{ key: "status", value: "published" }]
} }
@ -345,15 +357,35 @@ const results = await client.search.documents({
- Max length: 64 characters - Max length: 64 characters
- No spaces or special characters - No spaces or special characters
### Query Complexity Limits
- Maximum 200 conditions per query
- Maximum 8 levels of nested `AND`/`OR` expressions
<Note>
If you need more conditions than these limits allow, break your query into multiple requests or use broader search terms with post-processing.
</Note>
### Searching Within a Document
Use `docId` to scope a search to chunks within one large document — useful for books, podcasts, or other long-form content:
```typescript
const results = await client.search({
q: "machine learning",
docId: "doc_123"
});
```
--- ---
## Next Steps ## Next Steps
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Search" icon="search" href="/search"> <Card title="Search" icon="search" href="/recall/search">
Apply filters in search queries Apply filters in search queries
</Card> </Card>
<Card title="Add Memories" icon="plus" href="/add-memories"> <Card title="Add Memories" icon="plus" href="/ingestion/add-memories">
Add content with container tags and metadata Add content with container tags and metadata
</Card> </Card>
</CardGroup> </CardGroup>

View file

@ -1,146 +1,189 @@
--- ---
title: "How Graph Memory Works" title: "Graph memory"
sidebarTitle: "Graph Memory" sidebarTitle: "Graph memory"
description: "Automatic memory evolution, knowledge updates, and intelligent forgetting" description: "How facts connect, update, and stay true — memory relationships, temporal truth, and automatic forgetting."
icon: "vector-square" icon: "vector-square"
--- ---
Supermemory builds a living knowledge graph where memories connect to other memories. Unlike traditional knowledge graphs with entity-relation-entity triples, Supermemory's graph is **facts built on top of other facts**. **How understanding is stored and stays true over time.**
## Memory Relationships Supermemory builds a **living knowledge graph of facts on top of other facts** — not a static folder of embeddings, and not classic entityrelationentity triples you maintain by hand.
When you add content, Supermemory extracts facts and automatically connects them to existing memories through three relationship types: The **pipeline** that turns a chat or file into memories is [How it works](/concepts/how-it-works).
### Updates: Information Changes This page is the **model**: what a memory is, how edges form, and why agents utilize supermemory's graph
When new information contradicts existing knowledge: ## Try it
``` Get a key from the [developer console](https://console.supermemory.ai) — **API Keys → Create API Key** — then add a memory and pull it back with related edges:
Memory 1: "Alex works at Google as a software engineer"
Memory 2: "Alex just started at Stripe as a PM"
Memory 2 UPDATES Memory 1
```
The system tracks which memory is latest with `isLatest`, so searches return current information while preserving history.
### Extends: Information Enriches
When new information adds detail without replacing:
```
Memory 1: "Alex works at Stripe as a PM"
Memory 2: "Alex focuses on payments infrastructure and leads a team of 5"
Memory 2 EXTENDS Memory 1
```
Both memories remain valid—searches get richer context.
### Derives: Information Infers
When Supermemory infers new facts from patterns:
```
Memory 1: "Alex is a PM at Stripe"
Memory 2: "Alex frequently discusses payment APIs and fraud detection"
Derived: "Alex likely works on Stripe's core payments product"
```
These inferences surface insights you didn't explicitly state.
---
## Automatic Memory Extraction
From a single conversation, Supermemory extracts multiple connected memories:
**Input:**
> "Had a great call with Alex. He's enjoying the new PM role at Stripe, though the
> payments infrastructure work is intense. He moved to Seattle for the job—got a
> place in Capitol Hill. Wants to grab dinner next time I'm in town."
**Extracted memories:**
- Alex works at Stripe as a PM
- Alex works on payments infrastructure *(extends role memory)*
- Alex lives in Seattle, Capitol Hill *(new fact)*
- Alex wants to meet for dinner *(episodic)*
Each fact is connected to related memories automatically.
---
## Automatic Forgetting
Supermemory knows when memories become irrelevant:
**Time-based forgetting**: Temporary facts are automatically forgotten when they expire.
```
"I have an exam tomorrow"
After the exam date passes → automatically forgotten
"Meeting with Alex at 3pm today"
After today → automatically forgotten
```
**Contradiction resolution**: When new facts contradict old ones, the Update relationship ensures searches return current information.
**Noise filtering**: Casual, non-meaningful content doesn't become permanent memories.
---
## Memory Types
Supermemory distinguishes memory types automatically:
| Type | Example | Behavior |
|------|---------|----------|
| **Facts** | "Alex is a PM at Stripe" | Persists until updated |
| **Preferences** | "Alex prefers morning meetings" | Strengthens with repetition |
| **Episodes** | "Met Alex for coffee Tuesday" | Decays unless significant |
---
## What You Don't Do
All of this is automatic. You don't:
- Define relationships manually
- Tag memory types
- Clean up old memories
- Resolve contradictions
Just add content and search naturally:
```typescript ```typescript
import Supermemory from "supermemory";
const client = new Supermemory({ apiKey: "sm_..." }); // from console.supermemory.ai → API Keys
await client.add({ await client.add({
content: "Alex mentioned he just started at Stripe" content: "Alex mentioned he just started at Stripe",
containerTag: "user_123",
}); });
const results = await client.search({ const results = await client.search({
query: "where does Alex work?" q: "where does Alex work?",
containerTag: "user_123",
include: { relatedMemories: true },
}); });
// → Stripe (latest), previously Google (historical)
``` ```
--- Full walkthrough with a live example: [Quickstart](/quickstart).
## Learn More ![](/images/graph-view.png)
## Documents vs memories
| | **Documents** | **Memories** |
|---|---|---|
| **What** | Raw input you send | Facts Supermemory extracts |
| **Examples** | PDF, chat log, Drive file, URL | “Alex is a PM at Stripe” |
| **Role** | Source of truth for RAG / SuperRAG | Personal and entity state over time |
| **Lifecycle** | You add / update / delete | Graph updates, extends, derives, forgets |
Think of documents as books you hand the system. Memories are the insights it keeps — connected to each other as new content arrives.
<Note>
Uploading a long PDF does more than store bytes: Supermemory derives many memories and links them to what it already knows about that entity or user. Chunks of the document remain available for [SuperRAG](/concepts/super-rag) grounding.
</Note>
## Properties and rules of memories
1. Memories are atomic - Each memory has enough information and context about one particular topic
2. They always build on top of each other - with `updates`, the model knows the history, a memory `extends` from other memories, and new facts are derived (`derives` relation) from existing knowledg.e
## Memory relationships
![](/images/memories-inferred.png)
When content is processed, new facts connect to existing ones through three relationship types.
### Updates — information changes
New fact **replaces** what was true before for search purposes; history can remain for audit.
```text
Memory 1: "Alex works at Google as a software engineer"
Memory 2: "Alex just started at Stripe as a PM"
→ Memory 2 UPDATES Memory 1
```
`isLatest` (and related graph fields) keep retrieval on the current fact without erasing the past.
### Extends — information enriches
New fact **adds detail** without invalidating the old one.
```text
Memory 1: "Alex works at Stripe as a PM"
Memory 2: "Alex focuses on payments and leads a team of 5"
→ Memory 2 EXTENDS Memory 1
```
Both stay valid; context gets richer.
### Derives — information infers
Supermemory **infers** a fact you never stated in one place, from patterns across memories.
```text
Memory 1: "Alex is a PM at Stripe"
Memory 2: "Alex frequently discusses payment APIs and fraud detection"
→ Derived: "Alex likely works on Stripe's core payments product"
```
That is the same class of “entity chain” you see in the [quickstart](/quickstart) (gift → VP of Product → Sarah → Tokyo offsite). Search can expose edges via `include.relatedMemories` — see [Search API](/recall/search).
## Automatic extraction (one input → many facts)
**Input:**
> Had a great call with Alex. He's enjoying the new PM role at Stripe, though the payments work is intense. He moved to Seattle for the job—Capitol Hill. Wants dinner next time I'm in town.
**Example extracted memories:**
- Alex works at Stripe as a PM
- Alex works on payments infrastructure *(extends role)*
- Alex lives in Seattle, Capitol Hill
- Alex wants to meet for dinner *(episodic)*
You do not define schema or draw edges. You [add content](/ingestion/add-memories); the graph updates.
## Dreaming keeps the graph alive
Ingest is not a one-shot snapshot. After (and alongside) indexing, **dreaming** continues building the graph: extracting facts, linking related memories, resolving updates, and producing derives you never stated in one place.
By default Supermemory uses **`dreaming: "dynamic"`** — related documents are grouped so memories form from **coherent units** (e.g. a real multi-turn session), not each isolated write in isolation. That is why production quality is higher when you keep a stable `customId` on conversations and let dynamic dreaming do its job.
Use **`dreaming: "instant"`** when this document must hit the graph immediately (demos, “search right after add”). That path processes the document alone and costs an extra operation.
How to set the flag, statuses, and when `done` means what: [How it works → Dreaming](/concepts/how-it-works#dreaming-how-memories-enter-the-graph) and [Processing modes](/ingestion/add-memories#processing-modes).
## Memory types
| Type | Example | Behavior |
| --- | --- | --- |
| **Facts** | “Alex is a PM at Stripe” | Persists until updated |
| **Preferences** | “Alex prefers morning meetings” | Strengthens with repetition |
| **Episodes** | “Met Alex for coffee Tuesday” | Decays unless significant |
## Automatic forgetting
- **Time-based** — temporary facts drop after they expire (“exam tomorrow”, “meeting at 3pm today”).
- **Contradiction** — updates win for “whats true now.”
- **Noise filtering** — casual, non-meaningful chatter is less likely to become durable memory.
For explicit product controls (forget, review low-confidence derives), see [Forget & update](/recall/memory-operations) and [Memory review](/recall/memory-review).
## What you dont do
You do **not** hand-maintain the graph. You:
1. Ingest under a [container tag](/concepts/container-tags)
2. Wait for the [pipeline](/concepts/how-it-works) when needed
3. [Search](/recall/search) or load a [profile](/recall/user-profiles)
```typescript
await client.add({
content: "Alex mentioned he just started at Stripe",
containerTag: "user_123",
});
const results = await client.search({
q: "where does Alex work?",
containerTag: "user_123",
include: { relatedMemories: true },
});
// Prefer latest work fact (Stripe); history remains in the graph
```
## Related in the docs
| If you need… | Go to |
| --- | --- |
| Pipeline statuses, dreaming, documents in | [How it works](/concepts/how-it-works) |
| Memory vs document retrieval | [Memory vs RAG](/concepts/memory-vs-rag) · [SuperRAG](/concepts/super-rag) |
| Always-on summary of a user | [Profiles](/concepts/user-profiles) |
| Isolation / tenants | [Multi-tenancy](/concepts/container-tags) |
| API: add / search / forget | [Ingestion](/ingestion/add-memories) · [Search](/recall/search) · [Forget & update](/recall/memory-operations) |
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="How It Works" icon="cpu" href="/concepts/how-it-works"> <Card title="How it works" icon="cpu" href="/concepts/how-it-works">
Deep dive into the architecture Ingest pipeline, statuses, and outputs.
</Card> </Card>
<Card title="Memory vs RAG" icon="scale" href="/concepts/memory-vs-rag"> <Card title="Memory vs RAG" icon="scale" href="/concepts/memory-vs-rag">
When to use memory vs document retrieval When to use memory vs document retrieval.
</Card> </Card>
<Card title="User Profiles" icon="user" href="/user-profiles"> <Card title="Profiles" icon="user" href="/concepts/user-profiles">
Automatic summaries from the graph Static + dynamic context built from the graph.
</Card> </Card>
<Card title="Add Memories" icon="plus" href="/add-memories"> <Card title="Quickstart" icon="play" href="/quickstart">
Start building your knowledge graph See entity chains in a full conversation + document flow.
</Card> </Card>
</CardGroup> </CardGroup>

View file

@ -1,152 +1,182 @@
--- ---
title: "How Supermemory Works" title: "How Supermemory Works"
description: "Understanding the knowledge graph architecture that powers intelligent memory" sidebarTitle: "How it works"
description: "From a file or chat turn to something you can search — the ingest pipeline, statuses, and outputs."
icon: "cpu" icon: "cpu"
--- ---
At it's core, supermemory is powered by a custom learning model and a graph database that we built internally.
Supermemory isn't just another document storage system. It's designed to mirror how human memory actually works - forming connections, evolving over time, and generating insights from accumulated knowledge.
![](/images/graph-view.png)
## The Mental Model
Traditional systems store files. Supermemory creates a living knowledge graph.
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Traditional Systems" icon="folder"> <Card title="Learning model">
- Static files in folders Decides what and how to learn, what is important, when to forget, creating relations, etc.
- No connections between content
- Search matches keywords
- Information stays frozen
</Card> </Card>
<Card title="Temporal Vector-graph engine">
<Card title="Supermemory" icon="network"> Where the learnings are actually stored, optimized for search. Fact-based temporal graph that has Vector, FTS, and graph built in.
- Dynamic knowledge graph
- Rich relationships between memories
- Semantic understanding
- Information evolves and connects
</Card> </Card>
</CardGroup> </CardGroup>
## Documents vs Memories But, you don't have to think about the above. The interface for users is as simple as it gets.
Understanding this distinction is crucial to using Supermemory effectively. ## Get started in under a minute
### Documents: Your Raw Input
Documents are what you provide - the raw materials:
- PDF files you upload
- Web pages you save
- Text you paste
- Images with text
- Videos to transcribe
Think of documents as books you hand to Supermemory. See [Content Types](/concepts/content-types) for the full list of supported formats.
### Memories: Intelligent Knowledge Units
Memories are what Supermemory creates - the understanding:
- Semantic chunks with meaning
- Embedded for similarity search
- Connected through relationships
- Dynamically updated over time
Think of memories as the insights and connections your brain makes after reading those books.
<Note>
**Key Insight**: When you upload a 50-page PDF, Supermemory doesn't just store it. It breaks it into hundreds of interconnected memories, each understanding its context and relationships to your other knowledge.
</Note>
## Memory Relationships
![](/images/memories-inferred.png)
The graph connects memories through three types of relationships. For a deeper dive into how these relationships work, see [Graph Memory](/concepts/graph-memory).
### Updates: Information Changes
When new information contradicts or updates existing knowledge, Supermemory creates an "update" relationship.
<CodeGroup>
```text Original Memory
"You work at Supermemory as a content engineer"
```
```text New Memory (Updates Original)
"You now work at Supermemory as the CMO"
```
</CodeGroup>
The system tracks which memory is latest with an `isLatest` field, ensuring searches return current information.
### Extends: Information Enriches
When new information adds to existing knowledge without replacing it, Supermemory creates an "extends" relationship.
Continuing our "working at supermemory" analogy, a memory about what you work on would extend the memory about your role given above.
<CodeGroup>
```text Original Memory
"You work at Supermemory as the CMO"
```
```text New Memory (Extension) - Separate From Previous
"Your work consists of ensuring the docs are up to date, making marketing campaigns, SEO, etc."
```
</CodeGroup>
Both memories remain valid and searchable, providing richer context.
### Derives: Information Infers
The most sophisticated relationship - when Supermemory infers new connections from patterns in your knowledge.
<CodeGroup>
```text Memory 1
"Dhravya is the founder of Supermemory"
```
```text Memory 2
"Dhravya frequently discusses AI and machine learning innovations"
```
```text Derived Memory
"Supermemory is likely an AI-focused company"
```
</CodeGroup>
These inferences help surface insights you might not have explicitly stated.
## Processing Pipeline
Understanding the pipeline helps you optimize your usage:
| Stage | What Happens |
|-------|-------------|
| **Queued** | Document waiting to process
| **Extracting** | Content being extracted |
| **Chunking** | Creating memory chunks |
| **Embedding** | Generating vectors |
| **Indexing** | Building relationships |
| **Done** | Fully searchable |
<Note>
**Tip**: Larger documents and videos take longer. A 100-page PDF might take 1-2 minutes, while a 1-hour video could take 5-10 minutes.
</Note>
## Next Steps
Now that you understand how Supermemory works:
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Add Memories" icon="plus" href="/add-memories"> <Card title="1. Get an API key" icon="key" href="https://console.supermemory.ai">
Start adding content to your knowledge graph From the [developer console](https://console.supermemory.ai) — **API Keys → Create API Key**. `console.supermemory.ai` is where keys and usage live.
</Card> </Card>
<Card title="2. Use it" icon="terminal" href="/using-supermemory">
<Card title="Search Memories" icon="search" href="/search"> Install the SDK, drop in your key, add a memory, and search it — right below, or the full [ingest → retrieve loop](/using-supermemory).
Learn to query your knowledge effectively </Card>
</CardGroup>
<CodeGroup>
```bash TypeScript
npm install supermemory
```
```typescript TypeScript
import Supermemory from "supermemory";
const client = new Supermemory({ apiKey: "sm_..." }); // from console.supermemory.ai → API Keys
await client.add({ content: "The user loves Paris.", containerTag: "user_123" });
const { results } = await client.search({
q: "where does the user want to travel?",
containerTag: "user_123",
});
```
```python Python
from supermemory import Supermemory
client = Supermemory(api_key="sm_...") # from console.supermemory.ai → API Keys
client.add(content="The user loves Paris.", container_tag="user_123")
results = client.search(
q="where does the user want to travel?",
container_tag="user_123",
)
```
```bash curl
curl -X POST https://api.supermemory.ai/v3/documents \
-H "Authorization: Bearer sm_..." \
-H "Content-Type: application/json" \
-d '{
"content": "The user loves Paris.",
"containerTag": "user_123"
}'
```
</CodeGroup>
## What you send: documents
A **document** is raw input — whatever you hand Supermemory:
- Conversation transcripts and messages
- Text, markdown, HTML
- PDFs, images, audio/video, code
- URLs and connector items (Drive, Notion, Gmail, …)
You do not pre-chunk or pick an embedding model. See [Multi-modal ingestion](/concepts/content-types) for formats, and [Add context](/ingestion/add-memories) for the API.
Supermemory handles the ingestion and extraction for you. This also gives us a big advantage for quality - The engine extracts it in an optimized way with Contextual Chunking and other features for better quality search and memory generation.
> Use a stable **`customId`** when the same conversation or file will be updated later (sessions, connector syncs). That identity also drives [diff billing](/overview/billing#full-discount-on-already-seen-tokens-diff-billing) on re-ingest.
## What the pipeline does
| Stage | What happens |
| --- | --- |
| **Queued** | Accepted; waiting to run |
| **Extracting** | Text / OCR / transcription / page fetch |
| **Chunking** | Splits content for retrieval (type-aware where needed) |
| **Embedding** | Vectors for similarity search |
| **Indexing** | Makes chunks and derived structure searchable |
| **Done** | Document path is ready for search |
```typescript
const doc = await client.add({
content: conversationText,
containerTag: "user_123",
customId: "chat_session_1",
});
// Poll until ready
const status = await client.documents.get(doc.id);
// status.status → "queued" | "extracting" | ... | "done" | "failed"
```
Larger PDFs and long video take longer. Short chat turns usually finish in seconds.
## Dreaming (how memories enter the graph)
Document **status `done`** means chunks are indexed for search. **Memories** — the graph facts, updates, and derives — come from a second phase called **dreaming**.
This is when the content is passed through the memory model and merged, arranged and organized for the future.
Pass `dreaming` on [add](/ingestion/add-memories):
| Mode | Default? | Behavior | When to use |
| --- | --- | --- | --- |
| **`dynamic`** | Yes | Related documents are grouped so memories form from **coherent units**, not one isolated write at a time. Graph quality is higher for real multi-turn / multi-doc flows. Memory extraction may continue **after** `status: "done"`. | Production agents, connectors, ongoing sessions |
| **`instant`** | No | This document is dreamed **on its own, right away**. Memories are available as soon as processing finishes for that doc. Bills **one extra [operation](/overview/billing)** per document. | Demos, quickstarts, “I need the graph now” |
```typescript
// Production default — omit or set explicitly
await client.add({
content: conversationText,
containerTag: "user_123",
customId: "chat_session_1",
dreaming: "dynamic",
});
// Need memories immediately (e.g. tutorial)
await client.add({
content: conversationText,
containerTag: "user_123",
customId: "chat_session_1",
dreaming: "instant",
});
```
**Rule of thumb:** prefer **`dynamic`** for quality and cost in real apps, use **`instant`** when the next step is a memory search or profile that must reflect this document immediately (as in the [quickstart](/quickstart)). Keeping it dynamic helps it pair better with other memories and better connections, inferences to be made.
How those memories connect and stay true over time is [Graph memory](/concepts/graph-memory). API detail: [Processing modes](/ingestion/add-memories#processing-modes).
## What you get out
After the pipeline runs, the same document leads to three things -> Chunks, Memories and Profile. (in the same `containerTag`):
| Output | Role | Go deeper |
| --- | --- | --- |
| **Document chunks** | Grounding in the raw source (RAG / SuperRAG) | [SuperRAG](/concepts/super-rag), [Search API](/recall/search) |
| **Memories** | Extracted facts in a living graph — updates, links, time | [Graph memory](/concepts/graph-memory) |
| **Profile** | A sample of memories, static + dynamic summary for always-on context | [Profiles](/concepts/user-profiles), [Profile API](/recall/user-profiles) |
Supermemory does **not** only store the file. It derives **memories** (understanding) and keeps **chunks** (the source) so you can personalize *and* ground. That distinction is the core of [Memory vs RAG](/concepts/memory-vs-rag).
## Isolation and identity
- **`containerTag`** — hard isolation boundary (user, tenant, project). See [Container tags](/concepts/container-tags).
- **Metadata** — soft dimensions *inside* a tag for filtering. See [Metadata filtering](/concepts/filtering).
- **Scoped API keys** — credentials that cannot cross a container. See [API keys](/authentication#scoped-api-keys).
## Next steps
<CardGroup cols={2}>
<Card title="Graph memory" icon="vector-square" href="/concepts/graph-memory">
How facts connect, update, and stay true over time.
</Card>
<Card title="Multi-modal ingestion" icon="file-stack" href="/concepts/content-types">
Formats, extractors, and what you can send.
</Card>
<Card title="Add context" icon="plus" href="/ingestion/add-memories">
API: add, customId, files, dreaming, status.
</Card>
<Card title="Search API" icon="search" href="/recall/search">
Query documents and memories after the pipeline finishes.
</Card> </Card>
</CardGroup> </CardGroup>

View file

@ -216,10 +216,10 @@ client.add(
### 3. Hybrid Retrieval ### 3. Hybrid Retrieval
```python ```python
# Search combines both approaches # Search combines both approaches
results = client.documents.search( results = client.search.memories(
query="What phone should I recommend?", q="What phone should I recommend?",
container_tags=["user_123"], # Gets user memories container_tag="user_123", # Gets user memories
# Also searches general knowledge search_mode="hybrid", # Also searches general knowledge
) )
# Results include: # Results include:
@ -250,10 +250,10 @@ Supermemory provides both capabilities in a unified platform, ensuring your agen
<Card title="Super RAG" icon="bolt" href="/concepts/super-rag"> <Card title="Super RAG" icon="bolt" href="/concepts/super-rag">
Our managed RAG solution Our managed RAG solution
</Card> </Card>
<Card title="Add Memories" icon="plus" href="/add-memories"> <Card title="Add Memories" icon="plus" href="/ingestion/add-memories">
Start ingesting content Start ingesting content
</Card> </Card>
<Card title="Search" icon="search" href="/search"> <Card title="Search" icon="search" href="/recall/search">
Query your memories and documents Query your memories and documents
</Card> </Card>
</CardGroup> </CardGroup>

View file

@ -0,0 +1,134 @@
---
title: "Multi-tenancy Examples"
sidebarTitle: "Examples"
description: "Common container tag and metadata patterns for personal agents, company agents, email assistants, and support platforms"
icon: "list-checks"
---
A few common shapes multi-tenancy takes in practice, combining [container tags](/concepts/container-tags) for isolation with [metadata filters](/concepts/filtering) for organization within a boundary.
---
## Personal agent
A single container tag per user is enough — there's no shared data to leak, so metadata is optional.
```typescript
await client.add({
content: "User prefers morning workouts and vegetarian meals",
containerTag: "user_123",
});
const results = await client.search({
q: "workout preferences",
containerTag: "user_123",
});
```
---
## Company agent (shared + personal memory)
A company-wide assistant usually needs two kinds of containers: one **shared** container the whole org reads from, and one **personal** container per employee that nobody else can see.
```typescript
// Shared org knowledge — visible to everyone at the company
await client.add({
content: "Q3 roadmap: ship the mobile app redesign by end of August",
containerTag: "org_acme_shared",
metadata: { team: "product", type: "roadmap" },
});
// Personal memory — only this employee's agent should see this
await client.add({
content: "Prefers async updates over meetings",
containerTag: "org_acme_user_alex",
});
```
Inside the shared container, use metadata to scope queries to a team rather than creating a container tag per team:
```typescript
const results = await client.search({
q: "roadmap updates",
containerTag: "org_acme_shared",
searchMode: "documents",
filters: {
AND: [{ key: "team", value: "product" }],
},
});
```
An employee's agent typically queries both containers — their personal one plus the shared one — and merges the results, since the container tag boundary is per-request rather than per-user.
---
## Email assistant
One container tag per user, with metadata carrying email-specific properties like label, sender, or folder — so the assistant can answer things like *"find the Spotify email tagged Promotional"*.
```typescript
await client.add({
content: "Your Spotify Premium receipt for July — $11.99 charged",
containerTag: "user_123",
metadata: {
source: "gmail",
sender: "no-reply@spotify.com",
label: "Promotional",
},
});
const results = await client.search({
q: "spotify",
containerTag: "user_123",
searchMode: "documents",
filters: {
AND: [
{ key: "source", value: "gmail" },
{ key: "label", value: "Promotional" },
],
},
});
```
---
## Multi-tenant support platform
Each customer gets their own container tag, and metadata tracks ticket-level fields like status and priority — so "open, high-priority tickets" is a filter, not a new tag, and it can never accidentally include another customer's tickets.
```typescript
await client.add({
content: "Customer reports checkout button unresponsive on Safari",
containerTag: "org_customer_442",
metadata: { status: "open", priority: "high", channel: "chat" },
});
const results = await client.search({
q: "checkout issue",
containerTag: "org_customer_442",
searchMode: "documents",
filters: {
AND: [
{ key: "status", value: "open" },
{ key: "priority", value: "high" },
],
},
});
```
---
## Next steps
<CardGroup cols={2}>
<Card title="Multi-tenancy Overview" icon="users" href="/concepts/multi-tenancy">
Why container tags and metadata are separate mechanisms.
</Card>
<Card title="Container Tags" icon="folder" href="/concepts/container-tags">
How isolation works, naming rules, and access control.
</Card>
<Card title="Organizing & Filtering" icon="filter" href="/concepts/filtering">
Metadata filter types, combining `AND`/`OR`, and query limits.
</Card>
</CardGroup>

View file

@ -0,0 +1,114 @@
---
title: "Multi-tenancy Overview"
sidebarTitle: "Overview"
description: "How Supermemory isolates and organizes memories across users, tenants, and projects"
icon: "users"
---
Most apps built on Supermemory serve more than one user, customer, or tenant out of a single Supermemory organization. Multi-tenancy is how you keep those memories apart — so User A's data is never visible to User B, and so you can still slice and query within a user's own data by things like category, status, or date.
Supermemory gives you two complementary tools for this:
<CardGroup cols={2}>
<Card title="Container Tags" icon="folder" href="/concepts/container-tags">
**Isolation.** A container tag is a hard boundary — its own namespace. Memories in one tag are never returned by a search scoped to another tag.
</Card>
<Card title="Metadata Filtering" icon="database" href="/concepts/filtering">
**Organization.** Metadata is a set of custom key/value properties on a memory that you filter by — category, priority, date, participants, anything you define.
</Card>
</CardGroup>
They solve different problems, and most production apps use both together.
---
## Why two mechanisms
It's tempting to reach for one tool and make it do everything, but tags and metadata aren't interchangeable — they answer different questions.
| Question | Answer |
|----------|--------|
| "Which tenant does this memory belong to?" | **Container tag** |
| "Within this tenant's memories, which ones match `status: open`?" | **Metadata filter** |
| "Can this API key even see tenant X's data?" | **Container tag** (enforced as an access boundary) |
| "Find memories tagged `engineering` created after March" | **Metadata filter** |
A container tag decides **whether a memory is reachable at all** for a given request. Metadata decides **which of the reachable memories match**. Filtering never crosses a container tag boundary — you can't use metadata to peek into another tenant's container.
---
## How they work together
A typical multi-tenant write scopes the memory to a tenant with a container tag, then attaches metadata for finer-grained querying later:
```typescript
await client.add({
content: "Customer requested a refund for order #4821",
containerTag: "org_acme", // isolates to the "acme" tenant
metadata: {
category: "support",
status: "open",
priority: "high",
},
});
```
And a search combines both: the container tag restricts *which tenant's data* is in scope, and filters narrow down *which memories within that tenant* come back:
```typescript
const results = await client.search({
q: "refund request",
containerTag: "org_acme",
searchMode: "documents",
filters: {
AND: [
{ key: "category", value: "support" },
{ key: "status", value: "open" },
],
},
});
```
<Note>
Container tags are **required** for isolation and validated as an access boundary. Metadata filters are **optional** — a search with just `containerTag` and no `filters` still only returns that tenant's memories.
</Note>
---
## Choosing your boundary
Container tags are the layer that should map to your actual tenancy model — pick the level that matches what "one isolated space" means in your app:
| Pattern | Example | Use case |
|---------|---------|----------|
| Per-user | `user_{userId}` | Consumer app, personal memory per user |
| Per-tenant/org | `org_{orgId}` | B2B SaaS, one container per customer org |
| Hierarchical | `org:{orgId}:user:{userId}` | Multi-level — isolate by org, and optionally drill into a user within it |
| Per-project | `project_{projectId}` | Workspace- or project-scoped content |
Everything *within* that boundary — categories, statuses, dates, custom fields — is metadata, not a new tag. Don't create a new container tag for every property you want to filter on; that's what metadata is for.
---
## Access control
Container tags aren't just organizational — they're enforced as an authorization boundary. API keys and org members can be restricted to specific tags, so a request for a tag outside the caller's allowed set is rejected with `403 Forbidden` rather than silently filtered. See [Container Tags → Access control](/concepts/container-tags#access-control) for the details.
---
## Next steps
<CardGroup cols={2}>
<Card title="Examples" icon="list-checks" href="/concepts/multi-tenancy-examples">
Personal agents, company agents, email assistants, and support platforms.
</Card>
<Card title="Container Tags" icon="folder" href="/concepts/container-tags">
How isolation works, naming rules, and access control.
</Card>
<Card title="Organizing & Filtering" icon="filter" href="/concepts/filtering">
Metadata filter types, combining `AND`/`OR`, and query limits.
</Card>
<Card title="Scoped API keys" icon="key" href="/authentication#scoped-api-keys">
Mint keys that can only touch one container tag.
</Card>
</CardGroup>

View file

@ -0,0 +1,305 @@
---
title: "Rules of supermemory"
description: "Best practices and things to consider when using supermemory in your system"
sidebarTitle: "Rules of supermemory"
icon: "gavel"
---
Supermemory provides powerful primitives and the full context stack for building AI agents. This page collects rules of thumb from building and running supermemory in production. They aren't hard constraints, just shortcuts that save you time, cost, and confusing search results.
## Thinking about ingestion
### What to ingest, and what not to
#### Send what you would send to a human for memory
Treat supermemory as a database for human-like understanding of knowledge and search. You should be feeding it unstructured data like documents, chat conversations, or even images, videos, and websites. You should not be ingesting database records or CSVs, since those are more structured.
Although supermemory _does_ support learning from long-horizon structured data, typically the right approach is to give an agent tools to traverse the structure directly.
Agents benefit most from having a _general_ idea of the topic alongside tools to look through the data. For example, knowing "this company uses PostHog and has three products (API, Console, and Landing Page)" helps the agent navigate the PostHog data more effectively.
#### A quick test for where information belongs
| Context | Test result | Where it goes |
| --- | --- | --- |
| "Sarah prefers async updates and is being promoted to VP of Product" | A colleague would remember this | supermemory: [memory search](/recall/search) + profile |
| The Q3 planning doc, support tickets, the API changelog | A colleague would look it up by meaning | supermemory: ingested as documents, recalled with document search |
| Invoice #4821, total \$1,340.50, status `paid` | Queried by ID, summed in reports | your database |
| "Answer in the user's language. Never quote internal pricing." | Every request needs it, verbatim | system prompt |
Two things about this table that trip people up.
**"Remember" and "look up" are both supermemory, but different reads.** You [ingest documents](/ingestion/add-memories); the pipeline derives memories from them and maintains a profile per [container tag](/concepts/how-it-works). `client.search({ searchMode: "memories" })` recalls the derived facts. `client.search({ searchMode: "documents" })` recalls the source material itself. A support agent usually needs both: memories for "this customer runs self-hosted and already tried reinstalling", documents for the actual troubleshooting guide.
**Supermemory is not your system of record.** There's no SQL over memories, no joins, no aggregates, no querying by primary key. Keep transactional data in your database, and ingest the narrative *around* it ("the customer disputed invoice #4821 and churned over it") so your AI understands what the rows mean.
#### Ingest with SuperRag when you just need search
When you know you only want search, you can cut costs by 5x. Just set `taskType` when ingesting:
<CodeGroup>
```typescript TypeScript
await client.add({
content: "testing",
containerTag: "test",
taskType: "superrag"
});
```
```python Python
client.add(
content="testing",
container_tag="test",
task_type="superrag"
)
```
```bash curl
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "testing",
"containerTag": "test",
"taskType": "superrag"
}'
```
</CodeGroup>
#### Use hybrid mode when searching over SuperRag content
`hybrid` mode makes it much easier to get complete results from supermemory when you have both memories and documents.
<CodeGroup>
```typescript TypeScript
const results = await client.search({
q: "test",
searchMode: "hybrid"
});
```
```python Python
results = client.search.memories(
q="test",
search_mode="hybrid"
)
```
```bash curl
curl -X POST "https://api.supermemory.ai/v4/search" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "test",
"searchMode": "hybrid"
}'
```
</CodeGroup>
The response comes back in this shape:
```ts
({ memory: string } | { chunk: string })[]
```
Use `item.memory || item.chunk` when reading results.
#### Keep documents medium-sized
While supermemory can handle documents with 400k+ tokens, sending smaller, self-contained documents produces better-quality learnings. The internal learning agent and "dreaming" jobs reflect on memories to build relations between them. If documents are too long, fewer memories get generated and fewer relations get made.
We also recommend ingesting documents sequentially within a single `containerTag` where possible, since that's how supermemory determines what came first (used for `updates` relations and temporal reasoning).
#### Handling single-threaded chatbots
Many agent harnesses, like `openclaw`, `hermes`, and other single-threaded custom agents, run one long conversation with compaction. Some tips for managing single-threaded (and other long-running) conversations:
1. **Send a `customId` when you can**: a sessionId, conversationId, document ID, or any representation of a "session" in your application.
2. **Generate one if you don't have one**, e.g. the current 4-hour window: `${new Date().toISOString().slice(0,10)}-${new Date().getHours()>>2}`. Adjust the window size based on traffic per container.
3. **Send the same prefix**: keep the start of the document identical across ingests under the same `customId` so supermemory can diff cleanly. You can either resend the full growing transcript each time, or send only the new turns since your last ingest. Just don't mix the two for the same `customId`.
```
Ingestion 1:
Assistant: Hey, how are you?
User: I'm fine.
Ingestion 2 (full transcript):
Assistant: Hey, how are you?
User: I'm fine.
Assistant: Anything I can help with today?
Ingestion 2 (delta only):
Assistant: Anything I can help with today?
```
You're only billed for the new (diff) content you send, so doing this well improves performance, cuts cost, and keeps usage simple.
## Architecture and design
#### Let supermemory handle the learning
Don't pass content through an additional LLM before sending it to supermemory. Supermemory does that learning automatically. Because the engine already knows what it knows, it can contextually summarize, update, and forget information as needed.
#### Configure what you want it to learn
Ground it with `entityContext` to prevent drift over time. Picture a third person watching a conversation between two people: what do they remember, and about whom? Giving supermemory context about the entity itself helps ground its learnings and prevents drift and decay over time.
<CodeGroup>
```typescript TypeScript
const user = auth.user.name;
await client.add({
content: "Hey, I'm doing great!",
containerTag: user,
entityContext: `User is ${user}, talking to assistant Kira`
}); // -> supermemory learns "Dhravya is doing great"
```
```python Python
user = auth.user.name
client.add(
content="Hey, I'm doing great!",
container_tag=user,
entity_context=f"User is {user}, talking to assistant Kira"
) # -> supermemory learns "Dhravya is doing great"
```
```bash curl
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Hey, I'\''m doing great!",
"containerTag": "dhravya",
"entityContext": "User is dhravya, talking to assistant Kira"
}'
```
</CodeGroup>
#### Use containerTags, don't over-stuff a single one
Use a containerTag wherever there's a hard permission boundary.
- **Don't**: ingest everything into one container and filter through it with metadata.
- **Do**: give each user their own container, and still filter by metadata inside it if needed.
There's little correlation between the number of items in a container and its quality or latency. Supermemory is built for multi-tenant workloads and supports up to 1M documents and 10M memories per container.
#### Use metadata filtering for detailed scoping inside containers
You'll often want to ingest and search with filtering inside a single container. Say the engineering team ingests this:
<CodeGroup>
```typescript TypeScript
await client.add({
content: "The team prefers TypeScript",
metadata: { team: "Engineering" },
containerTag: "org-supermemory",
filterByMetadata: { team: "Engineering" }
});
```
```python Python
client.add(
content="The team prefers TypeScript",
metadata={"team": "Engineering"},
container_tag="org-supermemory",
filter_by_metadata={"team": "Engineering"}
)
```
```bash curl
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "The team prefers TypeScript",
"metadata": { "team": "Engineering" },
"containerTag": "org-supermemory",
"filterByMetadata": { "team": "Engineering" }
}'
```
</CodeGroup>
> Tip: `filterByMetadata` ensures a fact like "the team prefers TypeScript" is only built on top of the engineering team's knowledge.
Later, the research team ingests this, with the same `containerTag` but different `metadata`:
```json
{
"content": "The team prefers Python",
"metadata": { "team": "Research" },
"containerTag": "org-supermemory",
"filterByMetadata": { "team": "Research" }
}
```
This keeps research's and engineering's memories from mixing, even though they share a `containerTag`. When searching:
<CodeGroup>
```typescript TypeScript
const results = await client.search({
q: "preferred language",
containerTag: "org-supermemory",
searchMode: "documents",
filters: {
AND: [{ key: "team", value: "research" }]
}
}); // -> "python"
```
```python Python
results = client.search.documents(
q="preferred language",
container_tag="org-supermemory",
filters={
"AND": [{"key": "team", "value": "research"}]
}
) # -> "python"
```
```bash curl
curl -X POST "https://api.supermemory.ai/v3/search" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "preferred language",
"containerTag": "org-supermemory",
"filters": {
"AND": [{ "key": "team", "value": "research" }]
}
}'
```
</CodeGroup>
## Thinking about harness
Think about how to bring memory back into the harness itself.
#### Embrace a little noise
You might want to hyper-optimize everything that goes into the model's prompt, but counterintuitively, you sometimes want to embrace noise, since true personalization comes from distinctive information.
Example: a user says "hi" and the LLM responds "Hey Dhravya! How's it going? How's the new office coming along?" instead of something generic.
Supermemory is designed for this: it returns an average of 10 tokens per fact, so even 50 facts is just 500 tokens of context, cheap enough to stay generous.
#### Tools, hooks, and making the choice
Think about how supermemory fits into your harness. Example, a personal agent:
- **Session start hook** → load profile
- **On-message hook** → enrich the prompt with search
- **On-stop hook** → save the conversation
Play around with these options in our [playground](https://console.supermemory.ai/playground), and read more in [this post on memory at the harness level](https://dhravya.dev/writing/memory-on-the-harness-level/).

View file

@ -18,11 +18,10 @@ When you add content, Supermemory:
5. **Builds relationships** — Connects new knowledge to existing memories 5. **Builds relationships** — Connects new knowledge to existing memories
```typescript ```typescript
// Just add content — Supermemory handles the rest // Just upload — Supermemory handles the rest
await client.add({ await client.documents.uploadFile({
content: pdfBase64, file: fs.createReadStream('technical-documentation.pdf'),
contentType: "pdf", metadata: JSON.stringify({ title: "Technical Documentation" })
title: "Technical Documentation"
}); });
``` ```
@ -30,6 +29,63 @@ No chunking strategies to configure. No embedding models to choose. It just work
--- ---
## Ingesting as pure SuperRAG (`taskType: "superrag"`)
By default, every `client.add()` call runs on the **memory** path (`taskType: "memory"`): Supermemory chunks and embeds the content for retrieval, *and* runs it through the memory pipeline — extracting facts, updating the profile, and linking it into the knowledge graph.
If you're ingesting content that's purely reference material — documentation, a large PDF, a knowledge base article — and you don't need Supermemory to derive personal facts or update a profile from it, set `taskType: "superrag"`. It skips the memory pipeline entirely and only does the chunk → embed → index work needed to make the content searchable.
<CodeGroup>
```typescript TypeScript
await client.add({
content: "...", // e.g. a long internal wiki page
containerTag: "docs_kb",
taskType: "superrag",
});
```
```python Python
client.add(
content="...",
container_tag="docs_kb",
task_type="superrag",
)
```
```bash cURL
curl -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "...",
"containerTag": "docs_kb",
"taskType": "superrag"
}'
```
</CodeGroup>
| | `taskType: "memory"` (default) | `taskType: "superrag"` |
|---|---|---|
| Chunking, embedding, indexing | ✅ | ✅ — searchable immediately via `searchMode: "documents"` |
| Fact extraction into memories | ✅ | ❌ skipped |
| Profile (`static`/`dynamic`/buckets) updates | ✅ | ❌ skipped |
| Graph linking (updates/extends/derives) | ✅ | ❌ skipped |
| Price per ingested token | Full rate | **5x cheaper** |
<Tip>
`taskType: "superrag"` is a **5x discount on ingested tokens** — `sm_superrag_text`/`sm_superrag_rich` are priced at 20% of `sm_tokens_text`/`sm_tokens_rich`. See [Billing → Memory vs SuperRAG tokens](/overview/billing#memory-vs-superrag-tokens) for the exact rates.
</Tip>
<Warning>
Content ingested as `superrag` is retrievable via document search (`searchMode: "documents"`), but it will **never** surface as a memory, contribute to a user's profile, or connect into the knowledge graph. Use it for reference material you want searchable, not for anything that should shape what Supermemory knows about a user — that still needs the default `taskType: "memory"`.
</Warning>
When you're searching over a mix of both, `searchMode: "hybrid"` (below) is what pulls memory-path facts and superrag-path document chunks into one result set. More ingestion guidance: [Rules of supermemory → Ingest with SuperRag when you just need search](/concepts/rules#ingest-with-superrag-when-you-just-need-search).
---
## Smart Chunking by Content Type ## Smart Chunking by Content Type
Different content types need different chunking strategies. Supermemory applies the optimal approach automatically: Different content types need different chunking strategies. Supermemory applies the optimal approach automatically:
@ -170,7 +226,13 @@ You focus on building your product. Supermemory handles the RAG complexity.
<Card title="Memory vs RAG" icon="scale" href="/concepts/memory-vs-rag"> <Card title="Memory vs RAG" icon="scale" href="/concepts/memory-vs-rag">
When to use each approach When to use each approach
</Card> </Card>
<Card title="Search" icon="search" href="/search"> <Card title="Search" icon="search" href="/recall/search">
Search parameters and optimization Search parameters and optimization
</Card> </Card>
<Card title="Billing" icon="receipt" href="/overview/billing#memory-vs-superrag-tokens">
Exact meter rates for memory vs SuperRAG tokens
</Card>
<Card title="Adding Memories" icon="plus" href="/ingestion/add-memories">
`taskType` and other ingestion parameters
</Card>
</CardGroup> </CardGroup>

View file

@ -1,12 +1,16 @@
--- ---
title: "User Profiles" title: "User Profiles"
sidebarTitle: "User Profiles" sidebarTitle: "Profiles"
description: "Automatically maintained context about your users" description: "Automatically maintained context about your users"
icon: "circle-user" icon: "circle-user"
--- ---
User profiles are **automatically maintained collections of facts about your users** that Supermemory builds from all their interactions. Think of it as a persistent "about me" document that's always up-to-date. User profiles are **automatically maintained collections of facts about your users** that Supermemory builds from all their interactions. Think of it as a persistent "about me" document that's always up-to-date.
Each `containerTag` gets it's own profile.
> Note: It's called "user" profile, but in reality it can be anything - an agent, organization, etc.
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Instant Context" icon="bolt"> <Card title="Instant Context" icon="bolt">
No search needed — comprehensive user info always ready No search needed — comprehensive user info always ready
@ -30,6 +34,40 @@ Traditional memory systems rely entirely on search:
**Profiles provide the foundation**: Instead of searching for basic context, profiles give your LLM a complete picture of who the user is. **Profiles provide the foundation**: Instead of searching for basic context, profiles give your LLM a complete picture of who the user is.
![Search adds context to the prompt after a round trip; a profile rides along with every prompt for free](/images/user-profiles-vs-search.png)
A pure search architecture means every turn pays a `search(prompt)` round trip before the agent can respond. A profile is attached once and sits alongside every user prompt and agent output — no extra call, no latency, and no risk of the query missing something important.
---
## Non-literal-matching use cases
Semantic search retrieves content that's *similar to the query* — it's built for questions like "what did we discuss about the migration?" It's a poor fit for facts that should be known **regardless of what's being asked**, because there's rarely a query that's semantically close to them.
The clearest example is the user's own name. If someone tells your agent "call me Dhravya, not my full name" once during onboarding, that fact has almost nothing in common — vector-wise — with "help me plan a trip to Japan" or "review this PR." A search for either of those queries will not surface the name preference, because search only returns what's relevant to the query, and a name preference isn't relevant to trip planning or code review — it should just always be there.
```typescript
// Weeks earlier, during onboarding
await client.add({
content: "Call me Dhravya, not my full first name",
containerTag: "user_123",
});
// Later — an unrelated query
const results = await client.search({
q: "help me plan a trip to Japan",
containerTag: "user_123",
});
// The name preference won't be in `results` — it's not semantically
// related to trip planning, so search correctly leaves it out.
// But it's always in the profile, independent of the query:
const { profile } = await client.profile({ containerTag: "user_123" });
console.log(profile.static); // ["User goes by Dhravya, not their full name", ...]
```
This is the general pattern: names, pronouns, timezone, tone/format preferences, role, and other facts that should color *every* response — not just responses to a matching query — belong in the profile, not left to be caught by search. If your agent needs to "just know" something at all times, that's a strong signal it belongs in the profile rather than relying on a lucky semantic match.
--- ---
## Static vs Dynamic ## Static vs Dynamic
@ -54,17 +92,42 @@ Recent context and temporary states:
--- ---
## Buckets
Static and dynamic split facts by how long-lived they are. **Buckets** split them by *topic* — a third, independent axis you define, like `preferences`, `goals`, or `work`. As content is ingested, a classifier sorts each fact into the buckets it matches.
Every org starts with a default `preferences` bucket. Add your own in console settings at the organization level, or per space — space buckets are add-only, so a container tag always keeps every org-level bucket.
```typescript
const { profile } = await client.profile({
containerTag: "user_123",
include: ["buckets"],
buckets: ["preferences", "goals"], // optional — omit for all configured buckets
});
console.log(profile.buckets.preferences);
console.log(profile.buckets.goals);
```
Bucket descriptions steer the classifier, so a precise description ("explicit first-person preferences only, exclude inferred traits") produces cleaner buckets than a vague one. Buckets are separate from [`filterPrompt`](/concepts/customization), which controls what gets ingested at all — buckets only organize facts that already made it into the profile.
<Card title="Profile Buckets reference" icon="tags" href="/user-profiles/buckets">
Request bucketed profiles, create buckets at the org or space level, get AI-generated suggestions, and see validation limits.
</Card>
---
## How It Works ## How It Works
Profiles are built automatically through ingestion: Profiles are built automatically through ingestion:
1. **Ingest content** — Users [add documents](/add-memories), chat, or any content 1. **Ingest content** — Users [add documents](/ingestion/add-memories), chat, or any content
2. **Extract facts** — AI analyzes content for facts about the user 2. **Extract facts** — AI analyzes content for facts about the user
3. **Update profile** — System adds, updates, or removes facts 3. **Update profile** — System adds, updates, or removes facts
4. **Always current** — Profiles reflect the latest information 4. **Always current** — Profiles reflect the latest information
<Note> <Note>
You don't manually manage profiles — they build themselves as users interact. Start by [adding content](/add-memories) to see profiles in action. You don't manually manage profiles — they build themselves as users interact. Start by [adding content](/ingestion/add-memories) to see profiles in action.
</Note> </Note>
--- ---
@ -90,6 +153,36 @@ User asks: **"Can you help me debug this?"**
--- ---
## Filtering Profiles
Not many people realize this, but profiles support the same [metadata filtering](/concepts/filtering) as memory and document search. A profile is synthesized from the underlying memories in a container tag, so any `AND`/`OR` metadata filter you'd pass to `search` also narrows which memories are eligible to contribute to `static`, `dynamic`, and `buckets`.
```typescript
// Only build the profile from memories tagged as onboarding data
const { profile } = await client.profile({
containerTag: "user_123",
filters: {
AND: [{ key: "source", value: "onboarding" }],
},
});
```
This is useful when a container tag mixes memories from several sources or contexts and you only want one of them reflected in the profile — for example, a support agent that should only see profile facts derived from support tickets, not from an internal wiki synced into the same container:
```typescript
const { profile } = await client.profile({
containerTag: "org_customer_442",
filters: {
AND: [{ key: "channel", value: "support_ticket" }],
},
include: ["static", "dynamic"],
});
```
Filters apply on top of the search query too — combine `q` and `filters` to scope both the profile synthesis and the accompanying search results in one call. See [Filtering Profiles](/recall/user-profiles#filtering-profiles) for the full parameter reference.
---
## Use Cases ## Use Cases
### Personalized AI Assistants ### Personalized AI Assistants
@ -126,16 +219,19 @@ Profiles provide: preferred languages, coding style, current project context.
## Next Steps ## Next Steps
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="User Profiles API" icon="code" href="/user-profiles"> <Card title="User Profiles API" icon="code" href="/recall/user-profiles">
Fetch and use profiles via the API Fetch and use profiles via the API
</Card> </Card>
<Card title="Profile Buckets" icon="tags" href="/user-profiles/buckets">
Create and configure topical buckets
</Card>
<Card title="Graph Memory" icon="network" href="/concepts/graph-memory"> <Card title="Graph Memory" icon="network" href="/concepts/graph-memory">
How the underlying knowledge graph works How the underlying knowledge graph works
</Card> </Card>
<Card title="AI SDK Integration" icon="triangle" href="/integrations/ai-sdk"> <Card title="AI SDK Integration" icon="triangle" href="/integrations/ai-sdk">
Automatic profile injection with AI SDK Automatic profile injection with AI SDK
</Card> </Card>
<Card title="Add Memories" icon="plus" href="/add-memories"> <Card title="Add Memories" icon="plus" href="/ingestion/add-memories">
Build profiles by adding content Build profiles by adding content
</Card> </Card>
</CardGroup> </CardGroup>

View file

@ -1,7 +1,7 @@
--- ---
title: "GitHub Connector" title: "GitHub Connector"
description: "Connect GitHub repositories to sync documentation files into your Supermemory knowledge base" description: "Connect GitHub repositories to sync documentation files into your Supermemory knowledge base"
icon: "github" icon: "/images/github-icon.svg"
--- ---
Connect GitHub repositories to sync documentation files into your Supermemory knowledge base with OAuth authentication, webhook support, and automatic incremental syncing. Connect GitHub repositories to sync documentation files into your Supermemory knowledge base with OAuth authentication, webhook support, and automatic incremental syncing.
@ -25,7 +25,7 @@ The GitHub connector requires a **Scale Plan** or **Enterprise Plan**.
const connection = await client.connections.create('github', { const connection = await client.connections.create('github', {
redirectUrl: 'https://yourapp.com/auth/github/callback', redirectUrl: 'https://yourapp.com/auth/github/callback',
containerTags: ['user-123', 'github-sync'], containerTag: 'user-123',
documentLimit: 5000, documentLimit: 5000,
metadata: { metadata: {
source: 'github', source: 'github',
@ -48,7 +48,7 @@ The GitHub connector requires a **Scale Plan** or **Enterprise Plan**.
connection = client.connections.create( connection = client.connections.create(
'github', 'github',
redirect_url='https://yourapp.com/auth/github/callback', redirect_url='https://yourapp.com/auth/github/callback',
container_tags=['user-123', 'github-sync'], container_tag='user-123',
document_limit=10000, document_limit=10000,
metadata={ metadata={
'source': 'github', 'source': 'github',
@ -68,7 +68,7 @@ The GitHub connector requires a **Scale Plan** or **Enterprise Plan**.
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"redirectUrl": "https://yourapp.com/auth/github/callback", "redirectUrl": "https://yourapp.com/auth/github/callback",
"containerTags": ["user-123", "github-sync"], "containerTag": "user-123",
"documentLimit": 5000, "documentLimit": 5000,
"metadata": { "metadata": {
"source": "github", "source": "github",
@ -95,7 +95,7 @@ After the user grants permissions, GitHub redirects to your callback URL. The co
Unlike other connectors, GitHub requires repository selection before syncing begins. This gives your users control over which repositories to index. Unlike other connectors, GitHub requires repository selection before syncing begins. This gives your users control over which repositories to index.
<Note> <Note>
**Generic Endpoints:** GitHub uses the generic resource management endpoints (Get Resources and Configure Connection) that work for any provider supporting resource management. See [Managing Connection Resources](/memory-api/connectors/managing-resources) for detailed API documentation. **Generic Endpoints:** GitHub uses the generic resource management endpoints (Get Resources and Configure Connection) that work for any provider supporting resource management. See [Managing Connection Resources](/connectors/managing-resources) for detailed API documentation.
</Note> </Note>
<Tabs> <Tabs>

View file

@ -7,7 +7,7 @@ icon: "mail"
Connect Gmail to automatically sync email threads into your supermemory knowledge base. Supports real-time updates via Google Cloud Pub/Sub webhooks and incremental synchronization. Connect Gmail to automatically sync email threads into your supermemory knowledge base. Supports real-time updates via Google Cloud Pub/Sub webhooks and incremental synchronization.
<Note> <Note>
**Scale Plan Required:** The Gmail connector is available on Scale and Enterprise plans only. **Max Plan Required:** The Gmail connector is available on Max plan and above.
</Note> </Note>
## Quick Setup ## Quick Setup
@ -25,7 +25,7 @@ Connect Gmail to automatically sync email threads into your supermemory knowledg
const connection = await client.connections.create('gmail', { const connection = await client.connections.create('gmail', {
redirectUrl: 'https://yourapp.com/auth/gmail/callback', redirectUrl: 'https://yourapp.com/auth/gmail/callback',
containerTags: ['user-123', 'gmail-sync'], containerTag: 'user-123',
documentLimit: 5000, documentLimit: 5000,
metadata: { metadata: {
source: 'gmail', source: 'gmail',
@ -48,7 +48,7 @@ Connect Gmail to automatically sync email threads into your supermemory knowledg
connection = client.connections.create( connection = client.connections.create(
'gmail', 'gmail',
redirect_url='https://yourapp.com/auth/gmail/callback', redirect_url='https://yourapp.com/auth/gmail/callback',
container_tags=['user-123', 'gmail-sync'], container_tag='user-123',
document_limit=5000, document_limit=5000,
metadata={ metadata={
'source': 'gmail', 'source': 'gmail',
@ -68,7 +68,7 @@ Connect Gmail to automatically sync email threads into your supermemory knowledg
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"redirectUrl": "https://yourapp.com/auth/gmail/callback", "redirectUrl": "https://yourapp.com/auth/gmail/callback",
"containerTags": ["user-123", "gmail-sync"], "containerTag": "user-123",
"documentLimit": 5000, "documentLimit": 5000,
"metadata": { "metadata": {
"source": "gmail", "source": "gmail",
@ -90,7 +90,7 @@ After user grants permissions, Google redirects to your callback URL. The connec
```typescript ```typescript
// Get connection details // Get connection details
const connection = await client.connections.getByTags('gmail', { const connection = await client.connections.getByTags('gmail', {
containerTags: ['user-123', 'gmail-sync'] containerTags: ['user-123']
}); });
console.log('Connected email:', connection.email); console.log('Connected email:', connection.email);
@ -98,7 +98,7 @@ After user grants permissions, Google redirects to your callback URL. The connec
// List synced email threads // List synced email threads
const documents = await client.documents.list({ const documents = await client.documents.list({
containerTags: ['user-123', 'gmail-sync'] containerTags: ['user-123']
}); });
console.log(`Synced ${documents.memories.length} email threads`); console.log(`Synced ${documents.memories.length} email threads`);
@ -109,7 +109,7 @@ After user grants permissions, Google redirects to your callback URL. The connec
# Get connection details # Get connection details
connection = client.connections.get_by_tags( connection = client.connections.get_by_tags(
'gmail', 'gmail',
container_tags=['user-123', 'gmail-sync'] container_tags=['user-123']
) )
print(f'Connected email: {connection.email}') print(f'Connected email: {connection.email}')
@ -117,7 +117,7 @@ After user grants permissions, Google redirects to your callback URL. The connec
# List synced email threads # List synced email threads
documents = client.documents.list( documents = client.documents.list(
container_tags=['user-123', 'gmail-sync'] container_tags=['user-123']
) )
print(f'Synced {len(documents.memories)} email threads') print(f'Synced {len(documents.memories)} email threads')
@ -130,7 +130,7 @@ After user grants permissions, Google redirects to your callback URL. The connec
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"containerTags": ["user-123", "gmail-sync"], "containerTags": ["user-123"],
"provider": "gmail" "provider": "gmail"
}' }'
@ -139,7 +139,7 @@ After user grants permissions, Google redirects to your callback URL. The connec
-H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"containerTags": ["user-123", "gmail-sync"], "containerTags": ["user-123"],
"source": "gmail" "source": "gmail"
}' }'
``` ```
@ -177,9 +177,10 @@ Each synced thread includes searchable metadata:
You can filter searches using these metadata fields: You can filter searches using these metadata fields:
```typescript ```typescript
const results = await client.search.documents({ const results = await client.search({
q: "project update", q: "project update",
containerTags: ['user-123'], containerTag: 'user-123',
searchMode: "documents",
filters: JSON.stringify({ filters: JSON.stringify({
AND: [ AND: [
{ key: "type", value: "gmail_thread", negate: false }, { key: "type", value: "gmail_thread", negate: false },
@ -408,7 +409,7 @@ await client.connections.deleteByProvider('gmail', {
const newConnection = await client.connections.create('gmail', { const newConnection = await client.connections.create('gmail', {
redirectUrl: 'https://yourapp.com/auth/gmail/callback', redirectUrl: 'https://yourapp.com/auth/gmail/callback',
containerTags: ['user-123'] containerTag: 'user-123'
}); });
// User must re-authenticate // User must re-authenticate

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