convex component

This commit is contained in:
Sreeram Sreedhar 2026-04-20 13:41:01 -07:00
parent d38b0601a8
commit e4ec984aa1
17 changed files with 2265 additions and 23 deletions

View file

@ -0,0 +1,61 @@
name: Publish Convex Component
on:
push:
branches:
- main
paths:
- "packages/tools/src/convex-component/package.json"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./packages/tools/src/convex-component
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm for trusted publishing support
run: npm install -g npm@latest
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: bun install
- name: Check if version changed
id: version-check
run: |
PACKAGE_NAME=$(jq -r '.name' package.json)
LOCAL_VERSION=$(jq -r '.version' package.json)
NPM_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VERSION" = "$NPM_VERSION" ]; then
echo "Version $LOCAL_VERSION already published, skipping."
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "Publishing $LOCAL_VERSION (npm has $NPM_VERSION)"
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Build
if: steps.version-check.outputs.changed == 'true'
run: bun run build
- name: Publish
if: steps.version-check.outputs.changed == 'true'
run: npm publish --access public --provenance

View file

@ -162,6 +162,7 @@
"integrations/agent-framework",
"integrations/mastra",
"integrations/voltagent",
"integrations/convex",
"integrations/langchain",
"integrations/crewai",
"integrations/agno",

View file

@ -0,0 +1,377 @@
---
title: "Convex"
sidebarTitle: "Convex"
description: "Add semantic memory and RAG to your Convex apps with reactive queries and smart caching"
icon: "database"
---
Supermemory integrates with [Convex](https://convex.dev) as a native component, giving you AI-powered semantic memory with reactive real-time queries, smart caching, and full dashboard visibility.
<Card title="@supermemory/convex-component on npm" icon="npm" href="https://www.npmjs.com/package/@supermemory/convex-component">
Check out the NPM page for more details
</Card>
## Installation
```bash
npm install @supermemory/convex-component convex
```
## Quick Start
### 1. Setup Component
Create or update `convex/convex.config.ts`:
```typescript
import { defineApp } from "convex/server";
import supermemory from "@supermemory/convex-component/convex.config";
const app = defineApp();
app.use(supermemory, { name: "supermemory" });
export default app;
```
### 2. Set API Key
Get your API key from [console.supermemory.ai](https://console.supermemory.ai):
```bash
# Environment variable (recommended)
npx convex env set SUPERMEMORY_API_KEY your-api-key
# Or set it programmatically
npx convex run supermemory:mutations.setApiKey '{"apiKey": "your-api-key"}'
```
### 3. Use in Your App
<Tabs>
<Tab title="React Hooks">
```tsx
import { useAddMemory, useSupermemorySearch } from "@supermemory/convex-component/react";
function ChatApp() {
const addMemory = useAddMemory();
const { results, isLoading, search } = useSupermemorySearch({
q: "user preferences",
containerTag: "user_123",
searchMode: "hybrid"
});
const handleSendMessage = async (message: string) => {
await addMemory({
content: message,
containerTag: "user_123"
});
await search({ q: message, containerTag: "user_123" });
};
return (
<div>
{isLoading && <div>Searching memories...</div>}
{results?.results.map(r => (
<div key={r.id}>{r.memory || r.chunk}</div>
))}
</div>
);
}
```
</Tab>
<Tab title="TypeScript Client">
```typescript
import { ConvexHttpClient } from "convex/browser";
import { createSupermemoryClient } from "@supermemory/convex-component";
const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const supermemory = createSupermemoryClient(convex);
// Add a memory
await supermemory.add({
content: "User loves TypeScript and prefers tabs over spaces",
containerTag: "user_123",
metadata: { category: "preferences" }
});
// Search memories
const results = await supermemory.search({
q: "coding preferences",
containerTag: "user_123",
searchMode: "hybrid",
limit: 5
});
// Get user profile
const profile = await supermemory.profile({
containerTag: "user_123"
});
console.log("Static facts:", profile.profile.static);
console.log("Dynamic context:", profile.profile.dynamic);
```
</Tab>
</Tabs>
## React Hooks
### `useAddMemory()`
Add memories to Supermemory through a Convex action.
```tsx
const addMemory = useAddMemory();
await addMemory({
content: "Meeting notes from Q1 planning",
containerTag: "user_123",
customId: "meeting_2024_q1",
metadata: { type: "meeting" }
});
```
### `useSupermemorySearch(args)`
Reactive semantic search with loading and error states.
```tsx
const { results, isLoading, error, search } = useSupermemorySearch({
q: "project updates",
containerTag: "user_123",
searchMode: "hybrid",
limit: 10
});
// Trigger search manually
await search({ q: "new query", containerTag: "user_123" });
```
### `useSupermemoryProfile(args)`
Get user profile with static and dynamic facts.
```tsx
const { profile, isLoading, refresh } = useSupermemoryProfile({
containerTag: "user_123",
q: "recent preferences"
});
// Refresh profile
await refresh();
```
### `useDocumentList(args?)`
Reactively list all documents added to Supermemory.
```tsx
const documents = useDocumentList({ containerTag: "user_123", limit: 20 });
```
### `useMemories(args)`
List memories by container tag and source.
```tsx
const memories = useMemories({
containerTag: "user_123",
source: "manual", // "chat" | "document" | "manual"
limit: 50
});
```
### `useApiStats(args?)`
Get API call statistics for dashboard visibility.
```tsx
const stats = useApiStats({ containerTag: "user_123" });
// stats.totalCalls, stats.successfulCalls, stats.averageResponseTime
```
### `useApiLogs(args?)`
View recent API call logs for debugging.
```tsx
const logs = useApiLogs({ endpoint: "search", limit: 50 });
```
## Client SDK
For non-React environments or server-side code:
```typescript
import { ConvexHttpClient } from "convex/browser";
import { createSupermemoryClient } from "@supermemory/convex-component";
const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
const client = createSupermemoryClient(convex);
```
| Method | Description |
|--------|-------------|
| `client.add(args)` | Add content to Supermemory |
| `client.search(args)` | Semantic search across memories |
| `client.profile(args)` | Get user profile (static + dynamic facts) |
| `client.listDocuments(args?)` | List indexed documents |
| `client.getDocumentByCustomId(id)` | Get document by custom ID |
| `client.getApiLogs(args?)` | View API call logs |
| `client.getApiStats(args?)` | Get aggregated statistics |
| `client.searchCached(args)` | Local text search in Convex cache |
| `client.cleanCache()` | Remove expired cache entries |
| `client.setApiKey(key)` | Configure API key |
## AI SDK Integration
Use Supermemory with Vercel AI SDK through the Convex backend.
### Middleware (Automatic Context Injection)
```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { withSupermemory } from "@supermemory/convex-component/ai-sdk";
import { ConvexHttpClient } from "convex/browser";
const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
const modelWithMemory = withSupermemory(
openai("gpt-4"),
convex,
"user_123",
{ mode: "full", addMemory: "always", verbose: true }
);
const result = await generateText({
model: modelWithMemory,
messages: [{ role: "user", content: "What do you know about me?" }]
});
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `mode` | string | `"profile"` | `"profile"`, `"query"`, or `"full"` |
| `addMemory` | string | `"never"` | `"never"`, `"always"`, or `"tool"` |
| `promptTemplate` | function | — | Custom memory formatting function |
| `verbose` | boolean | `false` | Enable debug logging |
### Tools (Agent-Based Memory)
```typescript
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { supermemoryConvexTools } from "@supermemory/convex-component/ai-sdk";
import { ConvexHttpClient } from "convex/browser";
const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
const result = await streamText({
model: openai("gpt-4"),
prompt: "Remember that I love TypeScript",
tools: supermemoryConvexTools(convex, "user_123")
});
```
The tools include:
- **`searchMemories`** — Semantic search through user's memories and past conversations
- **`addMemory`** — Store new information about the user for future recall
## Advanced Usage
### Container Tags
Use container tags to isolate memories by user, session, or project:
```typescript
// Per user
await client.add({ content: "...", containerTag: "user_alice" });
await client.add({ content: "...", containerTag: "user_bob" });
// Per project
await client.add({ content: "...", containerTag: "project_123" });
```
### Metadata and Custom IDs
```typescript
// Add with metadata
await client.add({
content: "Design doc for feature X",
containerTag: "user_123",
customId: "design_doc_v2",
metadata: { type: "document", priority: "high" }
});
// Update existing content using the same customId
await client.add({
content: "Updated design doc for feature X",
containerTag: "user_123",
customId: "design_doc_v2" // Same ID = update
});
```
### Search with Filters
```typescript
const results = await client.search({
q: "design documents",
containerTag: "user_123",
searchMode: "hybrid",
threshold: 0.3,
rerank: true,
filters: {
AND: [
{ key: "type", value: "document" },
{ key: "priority", value: "high" }
]
}
});
```
## Smart Caching
The component automatically caches API responses in Convex:
| Cache | TTL | Purpose |
|-------|-----|---------|
| Search results | 5 minutes | Reduce redundant search API calls |
| User profiles | 2 minutes | Keep profiles fresh while limiting calls |
Cached data is served reactively — all connected clients see updates in real-time. Clean expired entries manually:
```typescript
const cleanCache = useCleanCache();
await cleanCache();
```
## Architecture
```
┌─────────────────────────────────────────────────────┐
│ Your React / Next.js App │
│ useSupermemorySearch, useAddMemory, useApiStats... │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Convex Backend │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Queries │ │Mutations │ │ Actions │ │
│ │(Reactive)│ │ (Cache) │ │ (Supermemory API) │ │
│ └─────┬────┘ └────┬─────┘ └────────┬─────────┘ │
│ └────────────┬┘ │ │
│ ┌──────────────────▼──────────────────┘ │
│ │ Convex Tables: searchCache, profileCache, │
│ │ documents, apiLogs, memories, analytics │
│ └──────────────────────────────────────────────────│
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Supermemory API (supermemory.ai) │
│ Semantic memory · User profiles · Hybrid search │
└─────────────────────────────────────────────────────┘
```

View file

@ -363,13 +363,49 @@ Check out the `/example` directory for a complete Next.js chat app with:
- Conversation memory
- API analytics dashboard
## Roadmap
## AI SDK Integration
- [ ] Vector search optimization
- [ ] Streaming responses
- [ ] Batch operations
- [ ] Analytics dashboard component
- [ ] Edge function support
Use Supermemory with Vercel AI SDK through the Convex backend:
### Middleware (Automatic Context Injection)
```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { withSupermemory } from "@supermemory/convex-component/ai-sdk";
import { ConvexHttpClient } from "convex/browser";
const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
const modelWithMemory = withSupermemory(
openai("gpt-4"),
convex,
"user_123",
{ mode: "full", addMemory: "always" }
);
const result = await generateText({
model: modelWithMemory,
messages: [{ role: "user", content: "What do you know about me?" }]
});
```
### Tools (Agent-Based Memory)
```typescript
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { supermemoryConvexTools } from "@supermemory/convex-component/ai-sdk";
import { ConvexHttpClient } from "convex/browser";
const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
const result = await streamText({
model: openai("gpt-4"),
prompt: "Remember that I love TypeScript",
tools: supermemoryConvexTools(convex, "user_123")
});
```
## Contributing

View file

@ -0,0 +1,738 @@
{
"name": "@supermemory/convex-component",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@supermemory/convex-component",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"ai": "6.0.168",
"convex": "^1.35.0",
"supermemory": "^4.21.1",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/react": "^19.2.14",
"react": "^19.0.0",
"typescript": "5.8.3"
},
"optionalDependencies": {
"ai": "^6.0.168",
"zod": "^3.25.76"
},
"peerDependencies": {
"convex": "^1.35.0",
"react": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"node_modules/@ai-sdk/gateway": {
"version": "3.0.104",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.104.tgz",
"integrity": "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.23",
"@vercel/oidc": "3.2.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/provider": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
"integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/provider-utils": {
"version": "4.0.23",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.23.tgz",
"integrity": "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz",
"integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz",
"integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz",
"integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz",
"integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz",
"integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz",
"integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz",
"integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz",
"integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz",
"integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz",
"integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz",
"integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz",
"integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz",
"integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz",
"integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz",
"integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz",
"integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz",
"integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz",
"integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz",
"integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz",
"integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz",
"integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz",
"integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz",
"integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz",
"integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz",
"integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz",
"integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@vercel/oidc": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz",
"integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">= 20"
}
},
"node_modules/ai": {
"version": "6.0.168",
"resolved": "https://registry.npmjs.org/ai/-/ai-6.0.168.tgz",
"integrity": "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.23",
"@opentelemetry/api": "1.9.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/convex": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/convex/-/convex-1.35.1.tgz",
"integrity": "sha512-g23KrTjBiXqRHzWIN0PVFagKjrmFxWUaOSiBsAWPTpXX2rXl0L1F4PR0YpAcMJEzMgfZR9AGymJvLTM+KA6lsQ==",
"license": "Apache-2.0",
"dependencies": {
"esbuild": "0.27.0",
"prettier": "^3.0.0",
"ws": "8.18.0"
},
"bin": {
"convex": "bin/main.js"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=7.0.0"
},
"peerDependencies": {
"@auth0/auth0-react": "^2.0.1",
"@clerk/clerk-react": "^4.12.8 || ^5.0.0",
"@clerk/react": "^6.0.0",
"react": "^18.0.0 || ^19.0.0-0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@auth0/auth0-react": {
"optional": true
},
"@clerk/clerk-react": {
"optional": true
},
"@clerk/react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz",
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.0",
"@esbuild/android-arm": "0.27.0",
"@esbuild/android-arm64": "0.27.0",
"@esbuild/android-x64": "0.27.0",
"@esbuild/darwin-arm64": "0.27.0",
"@esbuild/darwin-x64": "0.27.0",
"@esbuild/freebsd-arm64": "0.27.0",
"@esbuild/freebsd-x64": "0.27.0",
"@esbuild/linux-arm": "0.27.0",
"@esbuild/linux-arm64": "0.27.0",
"@esbuild/linux-ia32": "0.27.0",
"@esbuild/linux-loong64": "0.27.0",
"@esbuild/linux-mips64el": "0.27.0",
"@esbuild/linux-ppc64": "0.27.0",
"@esbuild/linux-riscv64": "0.27.0",
"@esbuild/linux-s390x": "0.27.0",
"@esbuild/linux-x64": "0.27.0",
"@esbuild/netbsd-arm64": "0.27.0",
"@esbuild/netbsd-x64": "0.27.0",
"@esbuild/openbsd-arm64": "0.27.0",
"@esbuild/openbsd-x64": "0.27.0",
"@esbuild/openharmony-arm64": "0.27.0",
"@esbuild/sunos-x64": "0.27.0",
"@esbuild/win32-arm64": "0.27.0",
"@esbuild/win32-ia32": "0.27.0",
"@esbuild/win32-x64": "0.27.0"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
"integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)",
"optional": true
},
"node_modules/prettier": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supermemory": {
"version": "4.21.1",
"license": "Apache-2.0",
"bin": {
"supermemory": "bin/cli"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"optional": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View file

@ -30,18 +30,19 @@
"import": "./dist/react/index.js",
"default": "./dist/react/index.js"
},
"./convex.config": {
"types": "./dist/component/convex.config.d.ts",
"import": "./dist/component/convex.config.js",
"default": "./dist/component/convex.config.js"
}
"./ai-sdk": {
"types": "./dist/ai-sdk/index.d.ts",
"import": "./dist/ai-sdk/index.js",
"default": "./dist/ai-sdk/index.js"
},
"./convex.config": "./dist/component/convex.config.ts"
},
"files": [
"dist",
"src"
],
"scripts": {
"build": "bun run build:component && bun run build:lib",
"build": "tsc && cp -r src/component dist/component",
"build:component": "cd src/component && npx convex dev --once --typecheck disable",
"build:lib": "tsc",
"dev": "cd src/component && npx convex dev",
@ -49,8 +50,12 @@
"check-types": "bun run typecheck"
},
"dependencies": {
"supermemory": "^4.21.1",
"convex": "^1.35.0"
"convex": "^1.35.0",
"supermemory": "^4.21.1"
},
"optionalDependencies": {
"ai": "^6.0.168",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/react": "^19.2.14",

View file

@ -0,0 +1,50 @@
/**
* Supermemory AI SDK Integration for Convex
*
* Integrate Supermemory with Vercel AI SDK using Convex as the backend.
* Provides both middleware (automatic context injection) and tools (agent-based memory).
*
* @example Middleware approach (automatic memory)
* ```typescript
* import { generateText } from "ai";
* import { openai } from "@ai-sdk/openai";
* import { withSupermemory } from "@supermemory/convex-component/ai-sdk";
* import { ConvexHttpClient } from "convex/browser";
*
* const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
* const modelWithMemory = withSupermemory(openai("gpt-4"), convex, "user_123");
*
* const result = await generateText({
* model: modelWithMemory,
* messages: [{ role: "user", content: "What do you know about me?" }]
* });
* ```
*
* @example Tools approach (AI controls memory)
* ```typescript
* import { streamText } from "ai";
* import { openai } from "@ai-sdk/openai";
* import { supermemoryConvexTools } from "@supermemory/convex-component/ai-sdk";
* import { ConvexHttpClient } from "convex/browser";
*
* const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
*
* const result = await streamText({
* model: openai("gpt-4"),
* prompt: "Remember that I love TypeScript",
* tools: supermemoryConvexTools(convex, "user_123")
* });
* ```
*/
export {
withSupermemory,
type SupermemoryOptions,
type MemoryPromptData,
} from "./middleware";
export {
supermemoryConvexTools,
searchMemoriesTool,
addMemoryTool,
} from "./tools";

View file

@ -0,0 +1,285 @@
import type { ConvexClient } from "convex/browser";
import type { FunctionReference } from "convex/server";
/**
* Supermemory AI SDK Middleware for Convex
*
* Wraps AI models to automatically inject user context from Convex-cached memories.
*/
/**
* Minimal language model interface for middleware wrapping.
* Compatible with Vercel AI SDK's LanguageModelV2/V3 without requiring
* a direct dependency on @ai-sdk/provider.
*/
interface WrappableLanguageModel {
doGenerate: (options: any) => Promise<any>;
doStream: (options: any) => Promise<any>;
[key: string]: unknown;
}
export interface SupermemoryOptions {
/**
* Memory retrieval mode
* - "profile": Get full user profile (static + dynamic facts)
* - "query": Search memories based on user's message
* - "full": Both profile AND query-based search
*/
mode?: "profile" | "query" | "full";
/**
* When to automatically save new memories
* - "never": Don't auto-save (default)
* - "always": Save every user message
* - "tool": Only when AI explicitly calls addMemory tool
*/
addMemory?: "never" | "always" | "tool";
/**
* Custom prompt template for formatting memories
*/
promptTemplate?: (data: MemoryPromptData) => string;
/**
* Enable verbose logging
*/
verbose?: boolean;
}
export interface MemoryPromptData {
userMemories: string;
generalSearchMemories: string;
searchResults: any[];
}
const DEFAULT_PROMPT_TEMPLATE = (data: MemoryPromptData) => `
# User Context
## User Profile
${data.userMemories}
## Relevant Memories
${data.generalSearchMemories}
Use this context to provide personalized, contextual responses.
`.trim();
/**
* Wrap an AI model with automatic Supermemory context injection
*
* @param model - The base AI model to wrap (any AI SDK language model)
* @param convexClient - Your Convex client instance
* @param containerTag - User/session identifier
* @param options - Configuration options
* @param componentPath - Path to the component (default: "supermemory")
*
* @example
* ```typescript
* import { generateText } from "ai";
* import { openai } from "@ai-sdk/openai";
* import { withSupermemory } from "@supermemory/convex-component/ai-sdk";
* import { ConvexHttpClient } from "convex/browser";
*
* const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
*
* const modelWithMemory = withSupermemory(
* openai("gpt-4"),
* convex,
* "user_123",
* { mode: "full", addMemory: "always" }
* );
*
* const result = await generateText({
* model: modelWithMemory,
* messages: [{ role: "user", content: "What do you know about me?" }]
* });
* ```
*/
export function withSupermemory<T extends WrappableLanguageModel>(
model: T,
convexClient: ConvexClient,
containerTag: string,
options: SupermemoryOptions = {},
componentPath: string = "supermemory"
): T {
const {
mode = "profile",
addMemory = "never",
promptTemplate = DEFAULT_PROMPT_TEMPLATE,
verbose = false,
} = options;
const profileAction = `${componentPath}:profile` as unknown as FunctionReference<"action">;
const searchAction = `${componentPath}:search` as unknown as FunctionReference<"action">;
const addAction = `${componentPath}:add` as unknown as FunctionReference<"action">;
return {
...model,
doGenerate: async (callOptions: any) => {
try {
// Extract user's last message for query-based search
const lastUserMessage = callOptions.prompt
.filter((msg: any) => msg.role === "user")
.slice(-1)[0];
const userQuery =
lastUserMessage && "content" in lastUserMessage
? typeof lastUserMessage.content === "string"
? lastUserMessage.content
: lastUserMessage.content.map((c: any) => (c.type === "text" ? c.text : "")).join(" ")
: "";
let userMemories = "";
let generalSearchMemories = "";
let searchResults: any[] = [];
// Fetch profile if needed
if (mode === "profile" || mode === "full") {
if (verbose) console.log("[Supermemory] Fetching user profile...");
const profile = await convexClient.action(profileAction, {
containerTag,
q: userQuery || undefined,
});
userMemories = [
...profile.profile.static.map((f: string) => `- ${f}`),
...profile.profile.dynamic.map((f: string) => `- ${f}`),
].join("\n");
if (verbose) {
console.log(`[Supermemory] Profile: ${profile.profile.static.length} static, ${profile.profile.dynamic.length} dynamic facts`);
}
}
// Query-based search if needed
if ((mode === "query" || mode === "full") && userQuery) {
if (verbose) console.log(`[Supermemory] Searching memories for: "${userQuery}"`);
const searchResult = await convexClient.action(searchAction, {
q: userQuery,
containerTag,
searchMode: "hybrid" as const,
limit: 5,
});
searchResults = searchResult.results;
generalSearchMemories = searchResults
.map((r: any) => `- ${r.memory || r.chunk} (similarity: ${r.similarity.toFixed(2)})`)
.join("\n");
if (verbose) {
console.log(`[Supermemory] Found ${searchResults.length} relevant memories (cached: ${searchResult.cached})`);
}
}
// Format context
const contextPrompt = promptTemplate({
userMemories,
generalSearchMemories,
searchResults,
});
// Inject context as system message
const enhancedPrompt = [
{ role: "system" as const, content: contextPrompt },
...callOptions.prompt,
];
// Auto-save user message if enabled
if (addMemory === "always" && userQuery) {
if (verbose) console.log("[Supermemory] Auto-saving user message...");
await convexClient.action(addAction, {
content: userQuery,
containerTag,
metadata: { source: "ai-middleware", auto: true },
});
}
// Call original model with enhanced context
return await model.doGenerate({
...callOptions,
prompt: enhancedPrompt,
});
} catch (error) {
console.error("[Supermemory] Error in middleware:", error);
// Fallback to original model without context on error
return await model.doGenerate(callOptions);
}
},
doStream: async (callOptions: any) => {
// For streaming, we inject context upfront then stream normally
try {
const lastUserMessage = callOptions.prompt
.filter((msg: any) => msg.role === "user")
.slice(-1)[0];
const userQuery =
lastUserMessage && "content" in lastUserMessage
? typeof lastUserMessage.content === "string"
? lastUserMessage.content
: lastUserMessage.content.map((c: any) => (c.type === "text" ? c.text : "")).join(" ")
: "";
let userMemories = "";
let generalSearchMemories = "";
let searchResults: any[] = [];
if (mode === "profile" || mode === "full") {
const profile = await convexClient.action(profileAction, {
containerTag,
q: userQuery || undefined,
});
userMemories = [
...profile.profile.static.map((f: string) => `- ${f}`),
...profile.profile.dynamic.map((f: string) => `- ${f}`),
].join("\n");
}
if ((mode === "query" || mode === "full") && userQuery) {
const searchResult = await convexClient.action(searchAction, {
q: userQuery,
containerTag,
searchMode: "hybrid" as const,
limit: 5,
});
searchResults = searchResult.results;
generalSearchMemories = searchResults
.map((r: any) => `- ${r.memory || r.chunk}`)
.join("\n");
}
const contextPrompt = promptTemplate({
userMemories,
generalSearchMemories,
searchResults,
});
const enhancedPrompt = [
{ role: "system" as const, content: contextPrompt },
...callOptions.prompt,
];
if (addMemory === "always" && userQuery) {
await convexClient.action(addAction, {
content: userQuery,
containerTag,
metadata: { source: "ai-middleware", auto: true },
});
}
return await model.doStream({
...callOptions,
prompt: enhancedPrompt,
});
} catch (error) {
console.error("[Supermemory] Error in streaming middleware:", error);
return await model.doStream(callOptions);
}
},
} as T;
}

View file

@ -0,0 +1,178 @@
import { tool } from "ai";
import { z } from "zod";
import type { ConvexClient } from "convex/browser";
import type { FunctionReference } from "convex/server";
/**
* Supermemory AI SDK Tools for Convex
*
* Provides AI agent tools that use Convex actions for memory operations.
* All operations are cached and tracked in the Convex dashboard.
*/
/**
* Create Supermemory tools for AI agents using Convex backend
*
* @param convexClient - Your Convex client instance
* @param containerTag - User/session identifier for memory isolation
* @param componentPath - Path to the component in your Convex config (default: "supermemory")
*
* @example
* ```typescript
* import { streamText } from "ai";
* import { openai } from "@ai-sdk/openai";
* import { supermemoryConvexTools } from "@supermemory/convex-component/ai-sdk";
* import { ConvexHttpClient } from "convex/browser";
*
* const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
*
* const result = await streamText({
* model: openai("gpt-4"),
* prompt: "Remember that I love TypeScript",
* tools: supermemoryConvexTools(convex, "user_123")
* });
* ```
*/
export function supermemoryConvexTools(
convexClient: ConvexClient,
containerTag: string,
componentPath: string = "supermemory"
): any {
const addAction = `${componentPath}:add` as unknown as FunctionReference<"action">;
const searchAction = `${componentPath}:search` as unknown as FunctionReference<"action">;
return {
/**
* Search through user's memories using semantic search
* The AI agent calls this when it needs to recall information
*/
// @ts-ignore - AI SDK v4 tool type compatibility
searchMemories: tool({
description:
"Search through the user's memories and past conversations. Use this to recall information the user has shared previously, their preferences, or relevant context from past interactions.",
parameters: z.object({
informationToGet: z
.string()
.describe(
"What information you're looking for. Be specific and use natural language (e.g., 'user dietary preferences', 'previous conversation about TypeScript')"
),
limit: z
.number()
.optional()
.describe("Maximum number of memories to retrieve (default: 5)"),
}),
// @ts-expect-error - AI SDK v4 tool type compatibility
execute: async ({ informationToGet, limit = 5 }: { informationToGet: string; limit?: number }) => {
try {
const result = await convexClient.action(searchAction, {
q: informationToGet,
containerTag,
searchMode: "hybrid" as const,
limit,
});
if (!result || result.results.length === 0) {
return {
success: true,
results: [],
count: 0,
message: "No relevant memories found",
};
}
return {
success: true,
results: result.results.map((r: any) => ({
content: r.memory || r.chunk,
similarity: r.similarity,
metadata: r.metadata,
})),
count: result.total,
cached: result.cached,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Search failed",
results: [],
count: 0,
};
}
},
}),
/**
* Add new information to user's memory
* The AI agent calls this when the user shares important information
*/
// @ts-ignore - AI SDK v4 tool type compatibility
addMemory: tool({
description:
"Store new information about the user that should be remembered for future conversations. Use this when the user shares preferences, facts about themselves, or important context that should be recalled later.",
parameters: z.object({
memory: z
.string()
.describe(
"The information to remember. Be clear and concise. Store facts, not full conversations (e.g., 'User is allergic to peanuts', 'User prefers dark mode')"
),
customId: z
.string()
.optional()
.describe(
"Optional unique identifier for this memory (useful for updating existing memories)"
),
metadata: z
.record(z.string(), z.any())
.optional()
.describe("Optional metadata for categorization or filtering"),
}),
// @ts-expect-error - AI SDK v4 tool type compatibility
execute: async ({ memory, customId, metadata }: { memory: string; customId?: string; metadata?: Record<string, any> }) => {
try {
const result = await convexClient.action(addAction, {
content: memory,
containerTag,
customId,
metadata,
});
return {
success: true,
memory: {
id: result.id,
status: result.status,
},
message: "Memory stored successfully",
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Failed to add memory",
};
}
},
}),
};
}
/**
* Individual tool exports for more granular control
*/
export function searchMemoriesTool(
convexClient: ConvexClient,
containerTag: string,
componentPath: string = "supermemory"
) {
return supermemoryConvexTools(convexClient, containerTag, componentPath)
.searchMemories;
}
export function addMemoryTool(
convexClient: ConvexClient,
containerTag: string,
componentPath: string = "supermemory"
) {
return supermemoryConvexTools(convexClient, containerTag, componentPath)
.addMemory;
}

View file

@ -18,7 +18,7 @@ export interface AddMemoryArgs {
export interface SearchMemoriesArgs {
q: string;
containerTag: string;
searchMode?: "hybrid" | "memories";
searchMode?: "hybrid" | "memories" | "documents";
limit?: number;
threshold?: number;
rerank?: boolean;

View file

@ -49,6 +49,21 @@ export const add = action({
status: result.status === "queued" ? "queued" : "processed",
});
// Store memory in dashboard table
await ctx.runMutation(internal.mutations.storeMemory, {
content: args.content,
containerTag: args.containerTag,
source: "manual",
supermemoryId: result.id,
metadata: args.metadata,
});
// Update analytics
await ctx.runMutation(internal.mutations.updateAnalytics, {
containerTag: args.containerTag,
incrementMemories: 1,
});
// Log API call
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "add",
@ -85,7 +100,7 @@ export const search = action({
args: {
q: v.string(),
containerTag: v.string(),
searchMode: v.optional(v.union(v.literal("hybrid"), v.literal("memories"))),
searchMode: v.optional(v.union(v.literal("hybrid"), v.literal("memories"), v.literal("documents"))),
limit: v.optional(v.number()),
threshold: v.optional(v.number()),
rerank: v.optional(v.boolean()),
@ -138,6 +153,13 @@ export const search = action({
ttl: 300, // 5 minutes
});
// Update analytics
await ctx.runMutation(internal.mutations.updateAnalytics, {
containerTag: args.containerTag,
incrementSearches: 1,
responseTime,
});
// Log API call
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "search",

View file

@ -6,5 +6,5 @@
*/
export { add, search, profile } from "./actions";
export { getApiStats, getApiLogs, listDocuments, getDocumentByCustomId } from "./queries";
export { cleanExpiredCache, setApiKey, updateDocumentStatus } from "./mutations";
export { getApiStats, getApiLogs, listDocuments, getDocumentByCustomId, listMemories, getChatSessions, getChatSession, getAnalytics, getDashboardOverview } from "./queries";
export { cleanExpiredCache, setApiKey, updateDocumentStatus, trackChatMessage } from "./mutations";

View file

@ -16,7 +16,7 @@ export const cacheSearchResults = internalMutation({
args: {
query: v.string(),
containerTag: v.string(),
searchMode: v.optional(v.union(v.literal("hybrid"), v.literal("memories"))),
searchMode: v.optional(v.union(v.literal("hybrid"), v.literal("memories"), v.literal("documents"))),
results: v.array(v.any()), // Accept any shape from Supermemory API
timing: v.number(),
total: v.number(),
@ -275,3 +275,169 @@ export const setApiKey = mutation({
}
},
});
/**
* Store a memory
* Tracks individual memories in the dashboard
*/
export const storeMemory = internalMutation({
args: {
content: v.string(),
containerTag: v.string(),
source: v.union(v.literal("chat"), v.literal("document"), v.literal("manual")),
supermemoryId: v.optional(v.string()),
metadata: v.optional(v.any()),
},
handler: async (ctx, args) => {
await ctx.db.insert("memories", {
content: args.content,
containerTag: args.containerTag,
source: args.source,
supermemoryId: args.supermemoryId,
createdAt: Date.now(),
metadata: args.metadata,
});
},
});
/**
* Create or update chat session
* Tracks conversation history with memory usage
*/
export const updateChatSession = internalMutation({
args: {
containerTag: v.string(),
sessionId: v.optional(v.id("chatSessions")),
newMessage: v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
content: v.string(),
timestamp: v.number(),
}),
memoriesRetrieved: v.array(v.string()),
},
handler: async (ctx, args) => {
if (args.sessionId) {
// Update existing session
const session = await ctx.db.get(args.sessionId);
if (session) {
await ctx.db.patch(args.sessionId, {
messages: [...session.messages, args.newMessage],
memoriesRetrieved: [
...new Set([...session.memoriesRetrieved, ...args.memoriesRetrieved])
],
lastMessageAt: args.newMessage.timestamp,
});
return args.sessionId;
}
}
// Create new session
const sessionId = await ctx.db.insert("chatSessions", {
containerTag: args.containerTag,
messages: [args.newMessage],
memoriesRetrieved: args.memoriesRetrieved,
createdAt: Date.now(),
lastMessageAt: args.newMessage.timestamp,
});
return sessionId;
},
});
/**
* Public wrapper for updateChatSession
* Allows clients to track chat sessions
*/
export const trackChatMessage = mutation({
args: {
containerTag: v.string(),
sessionId: v.optional(v.id("chatSessions")),
newMessage: v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
content: v.string(),
timestamp: v.number(),
}),
memoriesRetrieved: v.array(v.string()),
},
handler: async (ctx, args) => {
// Inline the logic instead of calling another mutation
if (args.sessionId) {
// Update existing session
const session = await ctx.db.get(args.sessionId);
if (session) {
await ctx.db.patch(args.sessionId, {
messages: [...session.messages, args.newMessage],
memoriesRetrieved: [
...new Set([...session.memoriesRetrieved, ...args.memoriesRetrieved])
],
lastMessageAt: args.newMessage.timestamp,
});
return args.sessionId;
}
}
// Create new session
const sessionId = await ctx.db.insert("chatSessions", {
containerTag: args.containerTag,
messages: [args.newMessage],
memoriesRetrieved: args.memoriesRetrieved,
createdAt: Date.now(),
lastMessageAt: args.newMessage.timestamp,
});
return sessionId;
},
});
/**
* Update analytics
* Updates usage statistics for a user
*/
export const updateAnalytics = internalMutation({
args: {
containerTag: v.string(),
incrementMemories: v.optional(v.number()),
incrementChats: v.optional(v.number()),
incrementSearches: v.optional(v.number()),
responseTime: v.optional(v.number()),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query("analytics")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.first();
if (existing) {
// Update existing analytics
const updates: any = {
lastActive: Date.now(),
};
if (args.incrementMemories) {
updates.totalMemories = existing.totalMemories + args.incrementMemories;
}
if (args.incrementChats) {
updates.totalChats = existing.totalChats + args.incrementChats;
}
if (args.incrementSearches) {
updates.totalSearches = existing.totalSearches + args.incrementSearches;
}
if (args.responseTime) {
// Calculate new average
const totalTime = existing.avgResponseTime * existing.totalSearches;
const newTotal = totalTime + args.responseTime;
updates.avgResponseTime = newTotal / (existing.totalSearches + 1);
}
await ctx.db.patch(existing._id, updates);
} else {
// Create new analytics entry
await ctx.db.insert("analytics", {
containerTag: args.containerTag,
totalMemories: args.incrementMemories || 0,
totalChats: args.incrementChats || 0,
totalSearches: args.incrementSearches || 0,
avgResponseTime: args.responseTime || 0,
lastActive: Date.now(),
});
}
},
});

View file

@ -204,3 +204,129 @@ export const searchCachedDocuments = query({
.slice(0, limit);
},
});
/**
* List memories for a user
* View all memories saved through Supermemory
*/
export const listMemories = query({
args: {
containerTag: v.string(),
source: v.optional(v.union(v.literal("chat"), v.literal("document"), v.literal("manual"))),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit || 50;
if (args.source) {
return await ctx.db
.query("memories")
.withIndex("by_source", (q) => q.eq("source", args.source))
.order("desc")
.filter((q) => q.eq(q.field("containerTag"), args.containerTag))
.take(limit);
}
return await ctx.db
.query("memories")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.order("desc")
.take(limit);
},
});
/**
* Get chat sessions for a user
* View conversation history with memory usage
*/
export const getChatSessions = query({
args: {
containerTag: v.string(),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit || 20;
return await ctx.db
.query("chatSessions")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.order("desc")
.take(limit);
},
});
/**
* Get a specific chat session
* View full conversation with memory usage
*/
export const getChatSession = query({
args: {
sessionId: v.id("chatSessions"),
},
handler: async (ctx, args) => {
return await ctx.db.get(args.sessionId);
},
});
/**
* Get analytics for a user
* View usage statistics and metrics
*/
export const getAnalytics = query({
args: {
containerTag: v.string(),
},
handler: async (ctx, args) => {
return await ctx.db
.query("analytics")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.first();
},
});
/**
* Get dashboard overview
* Comprehensive view of user's memory usage
*/
export const getDashboardOverview = query({
args: {
containerTag: v.string(),
},
handler: async (ctx, args) => {
const analytics = await ctx.db
.query("analytics")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.first();
const recentMemories = await ctx.db
.query("memories")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.order("desc")
.take(10);
const recentSessions = await ctx.db
.query("chatSessions")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.order("desc")
.take(5);
const recentDocuments = await ctx.db
.query("documents")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.order("desc")
.take(10);
return {
analytics: analytics || {
totalMemories: 0,
totalChats: 0,
totalSearches: 0,
avgResponseTime: 0,
lastActive: Date.now(),
},
recentMemories,
recentSessions,
recentDocuments,
};
},
});

View file

@ -15,7 +15,7 @@ export default defineSchema({
searchCache: defineTable({
query: v.string(),
containerTag: v.string(),
searchMode: v.optional(v.union(v.literal("hybrid"), v.literal("memories"))),
searchMode: v.optional(v.union(v.literal("hybrid"), v.literal("memories"), v.literal("documents"))),
results: v.any(), // Accept any shape from Supermemory API
timing: v.number(),
total: v.number(),
@ -96,4 +96,53 @@ export default defineSchema({
key: v.string(),
value: v.any(),
}).index("by_key", ["key"]),
/**
* Memories - Core memory storage
* All user memories saved through Supermemory
*/
memories: defineTable({
content: v.string(),
containerTag: v.string(),
source: v.union(v.literal("chat"), v.literal("document"), v.literal("manual")),
supermemoryId: v.optional(v.string()), // ID from Supermemory API
createdAt: v.number(),
metadata: v.optional(v.any()),
})
.index("by_container", ["containerTag"])
.index("by_source", ["source"])
.index("by_created", ["createdAt"]),
/**
* Chat Sessions - Conversation history with memory usage
* Tracks full conversations and which memories were retrieved
*/
chatSessions: defineTable({
containerTag: v.string(),
messages: v.array(
v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
content: v.string(),
timestamp: v.number(),
})
),
memoriesRetrieved: v.array(v.string()), // IDs of memories used in this session
createdAt: v.number(),
lastMessageAt: v.number(),
})
.index("by_container", ["containerTag"])
.index("by_last_message", ["lastMessageAt"]),
/**
* Analytics - Usage statistics per user
* Dashboard metrics for monitoring
*/
analytics: defineTable({
containerTag: v.string(),
totalMemories: v.number(),
totalChats: v.number(),
totalSearches: v.number(),
avgResponseTime: v.number(),
lastActive: v.number(),
}).index("by_container", ["containerTag"]),
});

View file

@ -342,6 +342,155 @@ export function useSetApiKey(componentPath: string = "supermemory") {
);
}
/**
* Hook to list memories for a user
*
* @param args - Filter arguments
* @param componentPath - Path to the component (default: "supermemory")
*
* @example
* ```tsx
* function MemoryList({ userId }) {
* const memories = useMemories({ containerTag: userId, limit: 50 });
*
* return (
* <div>
* {memories?.map(memory => (
* <div key={memory._id}>
* <p>{memory.content}</p>
* <span>Source: {memory.source}</span>
* </div>
* ))}
* </div>
* );
* }
* ```
*/
export function useMemories(
args: { containerTag: string; source?: "chat" | "document" | "manual"; limit?: number },
componentPath: string = "supermemory"
) {
const query = `${componentPath}:listMemories` as unknown as FunctionReference<"query">;
return useQuery(query, args) as any[] | undefined;
}
/**
* Hook to get chat sessions for a user
*
* @param args - Filter arguments
* @param componentPath - Path to the component (default: "supermemory")
*
* @example
* ```tsx
* function ChatHistory({ userId }) {
* const sessions = useChatSessions({ containerTag: userId, limit: 10 });
*
* return (
* <div>
* {sessions?.map(session => (
* <div key={session._id}>
* <p>{session.messages.length} messages</p>
* <p>Last active: {new Date(session.lastMessageAt).toLocaleString()}</p>
* </div>
* ))}
* </div>
* );
* }
* ```
*/
export function useChatSessions(
args: { containerTag: string; limit?: number },
componentPath: string = "supermemory"
) {
const query = `${componentPath}:getChatSessions` as unknown as FunctionReference<"query">;
return useQuery(query, args) as any[] | undefined;
}
/**
* Hook to get a specific chat session
*
* @param sessionId - Chat session ID
* @param componentPath - Path to the component (default: "supermemory")
*/
export function useChatSession(
sessionId: string | null,
componentPath: string = "supermemory"
) {
const query = `${componentPath}:getChatSession` as unknown as FunctionReference<"query">;
return useQuery(query, sessionId ? { sessionId } : "skip") as any | null | undefined;
}
/**
* Hook to get analytics for a user
*
* @param containerTag - User identifier
* @param componentPath - Path to the component (default: "supermemory")
*
* @example
* ```tsx
* function Analytics({ userId }) {
* const analytics = useAnalytics(userId);
*
* if (!analytics) return <div>Loading...</div>;
*
* return (
* <div>
* <p>Total Memories: {analytics.totalMemories}</p>
* <p>Total Chats: {analytics.totalChats}</p>
* <p>Total Searches: {analytics.totalSearches}</p>
* <p>Avg Response Time: {analytics.avgResponseTime.toFixed(0)}ms</p>
* </div>
* );
* }
* ```
*/
export function useAnalytics(
containerTag: string | null,
componentPath: string = "supermemory"
) {
const query = `${componentPath}:getAnalytics` as unknown as FunctionReference<"query">;
return useQuery(query, containerTag ? { containerTag } : "skip") as any | null | undefined;
}
/**
* Hook to get dashboard overview
*
* @param containerTag - User identifier
* @param componentPath - Path to the component (default: "supermemory")
*
* @example
* ```tsx
* function Dashboard({ userId }) {
* const overview = useDashboardOverview(userId);
*
* if (!overview) return <div>Loading...</div>;
*
* return (
* <div>
* <h2>Analytics</h2>
* <p>Total Memories: {overview.analytics.totalMemories}</p>
* <p>Total Chats: {overview.analytics.totalChats}</p>
*
* <h2>Recent Memories</h2>
* {overview.recentMemories.map(m => <p key={m._id}>{m.content}</p>)}
*
* <h2>Recent Sessions</h2>
* {overview.recentSessions.map(s => (
* <p key={s._id}>{s.messages.length} messages</p>
* ))}
* </div>
* );
* }
* ```
*/
export function useDashboardOverview(
containerTag: string | null,
componentPath: string = "supermemory"
) {
const query = `${componentPath}:getDashboardOverview` as unknown as FunctionReference<"query">;
return useQuery(query, containerTag ? { containerTag } : "skip") as any | undefined;
}
// Export all types
export type {
AddMemoryArgs,

View file

@ -16,9 +16,8 @@
"types": ["react"],
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
"isolatedModules": true
},
"include": ["src/client/**/*", "src/react/**/*"],
"include": ["src/client/**/*", "src/react/**/*", "src/ai-sdk/**/*"],
"exclude": ["node_modules", "dist", "src/component"]
}