fix: apply Biome formatting to convex-component

Ran `biome check --write` to fix formatting issues that were
failing CI. Warnings about noExplicitAny are expected and don't
fail the build.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
claude[bot] 2026-04-20 22:34:25 +00:00
parent d975943538
commit 1a63b97f3a
13 changed files with 1747 additions and 1618 deletions

View file

@ -38,13 +38,13 @@
*/
export {
withSupermemory,
type SupermemoryOptions,
type MemoryPromptData,
} from "./middleware";
withSupermemory,
type SupermemoryOptions,
type MemoryPromptData,
} from "./middleware"
export {
supermemoryConvexTools,
searchMemoriesTool,
addMemoryTool,
} from "./tools";
supermemoryConvexTools,
searchMemoriesTool,
addMemoryTool,
} from "./tools"

View file

@ -1,5 +1,5 @@
import type { ConvexClient } from "convex/browser";
import type { FunctionReference } from "convex/server";
import type { ConvexClient } from "convex/browser"
import type { FunctionReference } from "convex/server"
/**
* Supermemory AI SDK Middleware for Convex
@ -13,46 +13,47 @@ import type { FunctionReference } from "convex/server";
* a direct dependency on @ai-sdk/provider.
*/
interface WrappableLanguageModel {
doGenerate: (options: any) => Promise<any>;
doStream: (options: any) => Promise<any>;
[key: string]: unknown;
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";
/**
* 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";
/**
* 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;
/**
* Custom prompt template for formatting memories
*/
promptTemplate?: (data: MemoryPromptData) => string
/**
* Enable verbose logging
*/
verbose?: boolean;
/**
* Enable verbose logging
*/
verbose?: boolean
}
export interface MemoryPromptData {
userMemories: string;
generalSearchMemories: string;
searchResults: any[];
userMemories: string
generalSearchMemories: string
searchResults: any[]
}
const DEFAULT_PROMPT_TEMPLATE = (data: MemoryPromptData) => `
const DEFAULT_PROMPT_TEMPLATE = (data: MemoryPromptData) =>
`
# User Context
## User Profile
@ -62,7 +63,7 @@ ${data.userMemories}
${data.generalSearchMemories}
Use this context to provide personalized, contextual responses.
`.trim();
`.trim()
/**
* Wrap an AI model with automatic Supermemory context injection
@ -96,190 +97,205 @@ Use this context to provide personalized, contextual responses.
* ```
*/
export function withSupermemory<T extends WrappableLanguageModel>(
model: T,
convexClient: ConvexClient,
containerTag: string,
options: SupermemoryOptions = {},
componentPath: string = "supermemory"
model: T,
convexClient: ConvexClient,
containerTag: string,
options: SupermemoryOptions = {},
componentPath = "supermemory",
): T {
const {
mode = "profile",
addMemory = "never",
promptTemplate = DEFAULT_PROMPT_TEMPLATE,
verbose = false,
} = options;
const {
mode = "profile",
addMemory = "never",
promptTemplate = DEFAULT_PROMPT_TEMPLATE,
verbose = false,
} = options
const profileAction = `${componentPath}:actions.profile` as unknown as FunctionReference<"action">;
const searchAction = `${componentPath}:actions.search` as unknown as FunctionReference<"action">;
const addAction = `${componentPath}:actions.add` as unknown as FunctionReference<"action">;
const profileAction =
`${componentPath}:actions.profile` as unknown as FunctionReference<"action">
const searchAction =
`${componentPath}:actions.search` as unknown as FunctionReference<"action">
const addAction =
`${componentPath}:actions.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];
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(" ")
: "";
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[] = [];
let userMemories = ""
let generalSearchMemories = ""
let searchResults: any[] = []
// Fetch profile if needed
if (mode === "profile" || mode === "full") {
if (verbose) console.log("[Supermemory] Fetching user profile...");
// 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,
});
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");
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`);
}
}
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}"`);
// 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,
});
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");
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})`);
}
}
if (verbose) {
console.log(
`[Supermemory] Found ${searchResults.length} relevant memories (cached: ${searchResult.cached})`,
)
}
}
// Format context
const contextPrompt = promptTemplate({
userMemories,
generalSearchMemories,
searchResults,
});
// Format context
const contextPrompt = promptTemplate({
userMemories,
generalSearchMemories,
searchResults,
})
// Inject context as system message
const enhancedPrompt = [
{ role: "system" as const, content: contextPrompt },
...callOptions.prompt,
];
// 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...");
// 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 },
});
}
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);
}
},
// 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];
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(" ")
: "";
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[] = [];
let userMemories = ""
let generalSearchMemories = ""
let searchResults: any[] = []
if (mode === "profile" || mode === "full") {
const profile = await convexClient.action(profileAction, {
containerTag,
q: userQuery || undefined,
});
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");
}
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,
});
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");
}
searchResults = searchResult.results
generalSearchMemories = searchResults
.map((r: any) => `- ${r.memory || r.chunk}`)
.join("\n")
}
const contextPrompt = promptTemplate({
userMemories,
generalSearchMemories,
searchResults,
});
const contextPrompt = promptTemplate({
userMemories,
generalSearchMemories,
searchResults,
})
const enhancedPrompt = [
{ role: "system" as const, content: contextPrompt },
...callOptions.prompt,
];
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 },
});
}
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;
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

@ -1,7 +1,7 @@
import { tool } from "ai";
import { z } from "zod";
import type { ConvexClient } from "convex/browser";
import type { FunctionReference } from "convex/server";
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
@ -34,125 +34,142 @@ import type { FunctionReference } from "convex/server";
* ```
*/
export function supermemoryConvexTools(
convexClient: ConvexClient,
containerTag: string,
componentPath: string = "supermemory"
convexClient: ConvexClient,
containerTag: string,
componentPath = "supermemory",
): any {
const addAction = `${componentPath}:actions.add` as unknown as FunctionReference<"action">;
const searchAction = `${componentPath}:actions.search` as unknown as FunctionReference<"action">;
const addAction =
`${componentPath}:actions.add` as unknown as FunctionReference<"action">
const searchAction =
`${componentPath}:actions.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,
});
return {
/**
* Search through user's memories using semantic search
* The AI agent calls this when it needs to recall information
*/
// @ts-expect-error - 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",
};
}
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,
};
}
},
}),
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,
});
/**
* Add new information to user's memory
* The AI agent calls this when the user shares important information
*/
// @ts-expect-error - 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",
};
}
},
}),
};
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",
}
}
},
}),
}
}
/**
@ -160,19 +177,19 @@ export function supermemoryConvexTools(
*/
export function searchMemoriesTool(
convexClient: ConvexClient,
containerTag: string,
componentPath: string = "supermemory"
convexClient: ConvexClient,
containerTag: string,
componentPath = "supermemory",
) {
return supermemoryConvexTools(convexClient, containerTag, componentPath)
.searchMemories;
return supermemoryConvexTools(convexClient, containerTag, componentPath)
.searchMemories
}
export function addMemoryTool(
convexClient: ConvexClient,
containerTag: string,
componentPath: string = "supermemory"
convexClient: ConvexClient,
containerTag: string,
componentPath = "supermemory",
) {
return supermemoryConvexTools(convexClient, containerTag, componentPath)
.addMemory;
return supermemoryConvexTools(convexClient, containerTag, componentPath)
.addMemory
}

View file

@ -1,5 +1,5 @@
import type { ConvexClient } from "convex/browser";
import type { FunctionReference } from "convex/server";
import type { ConvexClient } from "convex/browser"
import type { FunctionReference } from "convex/server"
/**
* Supermemory Convex Client
@ -9,89 +9,89 @@ import type { FunctionReference } from "convex/server";
*/
export interface AddMemoryArgs {
content: string;
containerTag: string;
customId?: string;
metadata?: Record<string, any>;
content: string
containerTag: string
customId?: string
metadata?: Record<string, any>
}
export interface SearchMemoriesArgs {
q: string;
containerTag: string;
searchMode?: "hybrid" | "memories" | "documents";
limit?: number;
threshold?: number;
rerank?: boolean;
filters?: Record<string, any>;
q: string
containerTag: string
searchMode?: "hybrid" | "memories" | "documents"
limit?: number
threshold?: number
rerank?: boolean
filters?: Record<string, any>
}
export interface ProfileArgs {
containerTag: string;
q?: string;
containerTag: string
q?: string
}
export interface SearchResult {
id: string;
memory?: string;
chunk?: string;
similarity: number;
metadata?: Record<string, any>;
updatedAt: string;
version: number;
id: string
memory?: string
chunk?: string
similarity: number
metadata?: Record<string, any>
updatedAt: string
version: number
}
export interface SearchResponse {
results: SearchResult[];
timing: number;
total: number;
cached: boolean;
results: SearchResult[]
timing: number
total: number
cached: boolean
}
export interface ProfileResponse {
profile: {
static: string[];
dynamic: string[];
};
searchResults?: {
results: Array<{
id: string;
memory?: string;
chunk?: string;
similarity: number;
metadata?: Record<string, any>;
}>;
};
cached: boolean;
profile: {
static: string[]
dynamic: string[]
}
searchResults?: {
results: Array<{
id: string
memory?: string
chunk?: string
similarity: number
metadata?: Record<string, any>
}>
}
cached: boolean
}
export interface Document {
_id: string;
documentId: string;
customId?: string;
containerTag: string;
contentPreview: string;
metadata?: Record<string, any>;
status: "queued" | "processed" | "failed";
addedAt: number;
_id: string
documentId: string
customId?: string
containerTag: string
contentPreview: string
metadata?: Record<string, any>
status: "queued" | "processed" | "failed"
addedAt: number
}
export interface ApiLog {
_id: string;
endpoint: string;
containerTag?: string;
requestBody?: any;
responseStatus: "success" | "error" | "pending";
responseTime?: number;
errorMessage?: string;
timestamp: number;
_id: string
endpoint: string
containerTag?: string
requestBody?: any
responseStatus: "success" | "error" | "pending"
responseTime?: number
errorMessage?: string
timestamp: number
}
export interface ApiStats {
totalCalls: number;
successfulCalls: number;
failedCalls: number;
averageResponseTime: number;
callsByEndpoint: Record<string, number>;
totalCalls: number
successfulCalls: number
failedCalls: number
averageResponseTime: number
callsByEndpoint: Record<string, number>
}
/**
@ -122,121 +122,122 @@ export interface ApiStats {
* ```
*/
export function createSupermemoryClient(
client: ConvexClient,
componentPath: string = "supermemory"
client: ConvexClient,
componentPath = "supermemory",
) {
// Helper to construct function references
const action = (name: string): FunctionReference<"action"> => {
return `${componentPath}:actions.${name}` as any;
};
// Helper to construct function references
const action = (name: string): FunctionReference<"action"> => {
return `${componentPath}:actions.${name}` as any
}
const query = (name: string): FunctionReference<"query"> => {
return `${componentPath}:queries.${name}` as any;
};
const query = (name: string): FunctionReference<"query"> => {
return `${componentPath}:queries.${name}` as any
}
const mutation = (name: string): FunctionReference<"mutation"> => {
return `${componentPath}:mutations.${name}` as any;
};
const mutation = (name: string): FunctionReference<"mutation"> => {
return `${componentPath}:mutations.${name}` as any
}
return {
/**
* Add content to Supermemory
* Stores text, conversations, files, or URLs for semantic search
*/
add: async (args: AddMemoryArgs) => {
return await client.action(action("add"), args);
},
return {
/**
* Add content to Supermemory
* Stores text, conversations, files, or URLs for semantic search
*/
add: async (args: AddMemoryArgs) => {
return await client.action(action("add"), args)
},
/**
* Search memories and documents
* Performs semantic search across all content
*/
search: async (args: SearchMemoriesArgs): Promise<SearchResponse> => {
return await client.action(action("search"), args);
},
/**
* Search memories and documents
* Performs semantic search across all content
*/
search: async (args: SearchMemoriesArgs): Promise<SearchResponse> => {
return await client.action(action("search"), args)
},
/**
* Get user profile with context
* Retrieves static/dynamic facts about a user plus relevant memories
*/
profile: async (args: ProfileArgs): Promise<ProfileResponse> => {
return await client.action(action("profile"), args);
},
/**
* Get user profile with context
* Retrieves static/dynamic facts about a user plus relevant memories
*/
profile: async (args: ProfileArgs): Promise<ProfileResponse> => {
return await client.action(action("profile"), args)
},
/**
* List documents added to Supermemory
* Query documents with optional filtering
*/
listDocuments: async (args?: {
containerTag?: string;
limit?: number;
}): Promise<Document[]> => {
return await client.query(query("listDocuments"), args || {});
},
/**
* List documents added to Supermemory
* Query documents with optional filtering
*/
listDocuments: async (args?: {
containerTag?: string
limit?: number
}): Promise<Document[]> => {
return await client.query(query("listDocuments"), args || {})
},
/**
* Get a document by custom ID
*/
getDocumentByCustomId: async (customId: string): Promise<Document | null> => {
return await client.query(query("getDocumentByCustomId"), { customId });
},
/**
* Get a document by custom ID
*/
getDocumentByCustomId: async (
customId: string,
): Promise<Document | null> => {
return await client.query(query("getDocumentByCustomId"), { customId })
},
/**
* Get API call logs
* View recent Supermemory API calls for debugging
*/
getApiLogs: async (args?: {
endpoint?: string;
containerTag?: string;
limit?: number;
}): Promise<ApiLog[]> => {
return await client.query(query("getApiLogs"), args || {});
},
/**
* Get API call logs
* View recent Supermemory API calls for debugging
*/
getApiLogs: async (args?: {
endpoint?: string
containerTag?: string
limit?: number
}): Promise<ApiLog[]> => {
return await client.query(query("getApiLogs"), args || {})
},
/**
* Get API statistics
* Aggregate stats for dashboard visibility
*/
getApiStats: async (args?: {
containerTag?: string;
}): Promise<ApiStats> => {
return await client.query(query("getApiStats"), args || {});
},
/**
* Get API statistics
* Aggregate stats for dashboard visibility
*/
getApiStats: async (args?: {
containerTag?: string
}): Promise<ApiStats> => {
return await client.query(query("getApiStats"), args || {})
},
/**
* Search cached documents locally
* Fast text search across cached content
*/
searchCached: async (args: {
searchText: string;
containerTag?: string;
limit?: number;
}): Promise<Document[]> => {
return await client.query(query("searchCachedDocuments"), args);
},
/**
* Search cached documents locally
* Fast text search across cached content
*/
searchCached: async (args: {
searchText: string
containerTag?: string
limit?: number
}): Promise<Document[]> => {
return await client.query(query("searchCachedDocuments"), args)
},
/**
* Clean expired cache entries
* Removes old search and profile caches
*/
cleanCache: async () => {
return await client.mutation(mutation("cleanExpiredCache"), {});
},
/**
* Clean expired cache entries
* Removes old search and profile caches
*/
cleanCache: async () => {
return await client.mutation(mutation("cleanExpiredCache"), {})
},
/**
* Update document status
*/
updateDocumentStatus: async (args: {
documentId: string;
status: "queued" | "processed" | "failed";
}) => {
return await client.mutation(mutation("updateDocumentStatus"), args);
},
};
/**
* Update document status
*/
updateDocumentStatus: async (args: {
documentId: string
status: "queued" | "processed" | "failed"
}) => {
return await client.mutation(mutation("updateDocumentStatus"), args)
},
}
}
/**
* Type for the Supermemory client
*/
export type SupermemoryClient = ReturnType<typeof createSupermemoryClient>;
export type SupermemoryClient = ReturnType<typeof createSupermemoryClient>

View file

@ -1,7 +1,7 @@
import { action } from "./_generated/server";
import { v } from "convex/values";
import Supermemory from "supermemory";
import { api, internal } from "./_generated/api";
import { action } from "./_generated/server"
import { v } from "convex/values"
import Supermemory from "supermemory"
import { api, internal } from "./_generated/api"
/**
* Supermemory Actions
@ -15,262 +15,268 @@ import { api, internal } from "./_generated/api";
* Stores text, conversations, files, or URLs in Supermemory for semantic search
*/
export const add = action({
args: {
content: v.string(),
containerTag: v.string(),
customId: v.optional(v.string()),
metadata: v.optional(v.any()),
},
handler: async (ctx, args) => {
const startTime = Date.now();
args: {
content: v.string(),
containerTag: v.string(),
customId: v.optional(v.string()),
metadata: v.optional(v.any()),
},
handler: async (ctx, args) => {
const startTime = Date.now()
try {
// Get API key from config
const apiKey = await ctx.runQuery(internal.lib.getApiKey);
const client = new Supermemory({ apiKey });
try {
// Get API key from config
const apiKey = await ctx.runQuery(internal.lib.getApiKey)
const client = new Supermemory({ apiKey })
// Call Supermemory API
const result = await client.add({
content: args.content,
containerTag: args.containerTag,
customId: args.customId,
metadata: args.metadata,
});
// Call Supermemory API
const result = await client.add({
content: args.content,
containerTag: args.containerTag,
customId: args.customId,
metadata: args.metadata,
})
const responseTime = Date.now() - startTime;
const responseTime = Date.now() - startTime
// Store document metadata in Convex
await ctx.runMutation(internal.mutations.storeDocument, {
documentId: result.id,
customId: args.customId,
containerTag: args.containerTag,
contentPreview: args.content.substring(0, 200),
metadata: args.metadata,
status: result.status === "queued" ? "queued" : "processed",
});
// Store document metadata in Convex
await ctx.runMutation(internal.mutations.storeDocument, {
documentId: result.id,
customId: args.customId,
containerTag: args.containerTag,
contentPreview: args.content.substring(0, 200),
metadata: args.metadata,
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,
});
// 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,
});
// Update analytics
await ctx.runMutation(internal.mutations.updateAnalytics, {
containerTag: args.containerTag,
incrementMemories: 1,
})
// Log API call
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "add",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "success",
responseTime,
});
// Log API call
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "add",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "success",
responseTime,
})
return result;
} catch (error) {
const responseTime = Date.now() - startTime;
return result
} catch (error) {
const responseTime = Date.now() - startTime
// Log error
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "add",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "error",
responseTime,
errorMessage: error instanceof Error ? error.message : "Unknown error",
});
// Log error
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "add",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "error",
responseTime,
errorMessage: error instanceof Error ? error.message : "Unknown error",
})
throw error;
}
},
});
throw error
}
},
})
/**
* Search memories and documents
* Performs semantic search across all content in Supermemory
*/
export const search = action({
args: {
q: v.string(),
containerTag: v.string(),
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()),
filters: v.optional(v.any()),
},
handler: async (ctx, args) => {
const startTime = Date.now();
args: {
q: v.string(),
containerTag: v.string(),
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()),
filters: v.optional(v.any()),
},
handler: async (ctx, args) => {
const startTime = Date.now()
try {
// Check cache first
const cached = await ctx.runQuery(api.queries.getSearchCache, {
query: args.q,
containerTag: args.containerTag,
searchMode: args.searchMode,
});
try {
// Check cache first
const cached = await ctx.runQuery(api.queries.getSearchCache, {
query: args.q,
containerTag: args.containerTag,
searchMode: args.searchMode,
})
if (cached) {
return {
results: cached.results,
timing: cached.timing,
total: cached.total,
cached: true,
};
}
if (cached) {
return {
results: cached.results,
timing: cached.timing,
total: cached.total,
cached: true,
}
}
// Get API key from config
const apiKey = await ctx.runQuery(internal.lib.getApiKey);
const client = new Supermemory({ apiKey });
// Get API key from config
const apiKey = await ctx.runQuery(internal.lib.getApiKey)
const client = new Supermemory({ apiKey })
// Call Supermemory API
const result = await client.search.memories({
q: args.q,
containerTag: args.containerTag,
searchMode: args.searchMode || "hybrid",
limit: args.limit,
threshold: args.threshold,
rerank: args.rerank,
filters: args.filters,
});
// Call Supermemory API
const result = await client.search.memories({
q: args.q,
containerTag: args.containerTag,
searchMode: args.searchMode || "hybrid",
limit: args.limit,
threshold: args.threshold,
rerank: args.rerank,
filters: args.filters,
})
const responseTime = Date.now() - startTime;
const responseTime = Date.now() - startTime
// Cache results (expires in 5 minutes)
await ctx.runMutation(internal.mutations.cacheSearchResults, {
query: args.q,
containerTag: args.containerTag,
searchMode: args.searchMode,
results: result.results,
timing: result.timing,
total: result.total,
ttl: 300, // 5 minutes
});
// Cache results (expires in 5 minutes)
await ctx.runMutation(internal.mutations.cacheSearchResults, {
query: args.q,
containerTag: args.containerTag,
searchMode: args.searchMode,
results: result.results,
timing: result.timing,
total: result.total,
ttl: 300, // 5 minutes
})
// Update analytics
await ctx.runMutation(internal.mutations.updateAnalytics, {
containerTag: args.containerTag,
incrementSearches: 1,
responseTime,
});
// 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",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "success",
responseTime,
});
// Log API call
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "search",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "success",
responseTime,
})
return {
...result,
cached: false,
};
} catch (error) {
const responseTime = Date.now() - startTime;
return {
...result,
cached: false,
}
} catch (error) {
const responseTime = Date.now() - startTime
// Log error
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "search",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "error",
responseTime,
errorMessage: error instanceof Error ? error.message : "Unknown error",
});
// Log error
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "search",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "error",
responseTime,
errorMessage: error instanceof Error ? error.message : "Unknown error",
})
throw error;
}
},
});
throw error
}
},
})
/**
* Get user profile with context
* Retrieves static/dynamic facts about a user plus relevant memories
*/
export const profile = action({
args: {
containerTag: v.string(),
q: v.optional(v.string()),
},
handler: async (ctx, args) => {
const startTime = Date.now();
args: {
containerTag: v.string(),
q: v.optional(v.string()),
},
handler: async (ctx, args) => {
const startTime = Date.now()
try {
// Check cache first
const cached = await ctx.runQuery(api.queries.getProfileCache, {
containerTag: args.containerTag,
});
try {
// Check cache first
const cached = await ctx.runQuery(api.queries.getProfileCache, {
containerTag: args.containerTag,
})
if (cached) {
return {
profile: {
static: cached.staticProfile,
dynamic: cached.dynamicProfile,
},
searchResults: cached.searchResults
? { results: cached.searchResults }
: undefined,
cached: true,
};
}
if (cached) {
return {
profile: {
static: cached.staticProfile,
dynamic: cached.dynamicProfile,
},
searchResults: cached.searchResults
? { results: cached.searchResults }
: undefined,
cached: true,
}
}
// Get API key from config
const apiKey = await ctx.runQuery(internal.lib.getApiKey);
const client = new Supermemory({ apiKey });
// Get API key from config
const apiKey = await ctx.runQuery(internal.lib.getApiKey)
const client = new Supermemory({ apiKey })
// Call Supermemory API
const result = await client.profile({
containerTag: args.containerTag,
q: args.q,
});
// Call Supermemory API
const result = await client.profile({
containerTag: args.containerTag,
q: args.q,
})
const responseTime = Date.now() - startTime;
const responseTime = Date.now() - startTime
// Cache profile (expires in 2 minutes for freshness)
await ctx.runMutation(internal.mutations.cacheProfile, {
containerTag: args.containerTag,
staticProfile: result.profile.static,
dynamicProfile: result.profile.dynamic,
searchResults: result.searchResults?.results,
ttl: 120, // 2 minutes
});
// Cache profile (expires in 2 minutes for freshness)
await ctx.runMutation(internal.mutations.cacheProfile, {
containerTag: args.containerTag,
staticProfile: result.profile.static,
dynamicProfile: result.profile.dynamic,
searchResults: result.searchResults?.results,
ttl: 120, // 2 minutes
})
// Log API call
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "profile",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "success",
responseTime,
});
// Log API call
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "profile",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "success",
responseTime,
})
return {
...result,
cached: false,
};
} catch (error) {
const responseTime = Date.now() - startTime;
return {
...result,
cached: false,
}
} catch (error) {
const responseTime = Date.now() - startTime
// Log error
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "profile",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "error",
responseTime,
errorMessage: error instanceof Error ? error.message : "Unknown error",
});
// Log error
await ctx.runMutation(internal.mutations.logApiCall, {
endpoint: "profile",
containerTag: args.containerTag,
requestBody: args,
responseStatus: "error",
responseTime,
errorMessage: error instanceof Error ? error.message : "Unknown error",
})
throw error;
}
},
});
throw error
}
},
})

View file

@ -1,4 +1,4 @@
import { defineComponent } from "convex/server";
import { defineComponent } from "convex/server"
/**
* Supermemory Convex Component
@ -7,4 +7,4 @@ import { defineComponent } from "convex/server";
* into your Convex application, providing reactive access to AI-powered memory,
* user profiles, and semantic search.
*/
export default defineComponent("supermemory");
export default defineComponent("supermemory")

View file

@ -5,6 +5,20 @@
* All functions listed here will be accessible from the client.
*/
export { add, search, profile } from "./actions";
export { getApiStats, getApiLogs, listDocuments, getDocumentByCustomId, listMemories, getChatSessions, getChatSession, getAnalytics, getDashboardOverview } from "./queries";
export { cleanExpiredCache, updateDocumentStatus, trackChatMessage } from "./mutations";
export { add, search, profile } from "./actions"
export {
getApiStats,
getApiLogs,
listDocuments,
getDocumentByCustomId,
listMemories,
getChatSessions,
getChatSession,
getAnalytics,
getDashboardOverview,
} from "./queries"
export {
cleanExpiredCache,
updateDocumentStatus,
trackChatMessage,
} from "./mutations"

View file

@ -1,4 +1,4 @@
import { internalQuery } from "./_generated/server";
import { internalQuery } from "./_generated/server"
/**
* Internal library functions
@ -11,26 +11,26 @@ import { internalQuery } from "./_generated/server";
* Used by actions to authenticate with Supermemory API
*/
export const getApiKey = internalQuery({
args: {},
handler: async (ctx): Promise<string> => {
// Check Convex environment variable first
const envApiKey = process.env.SUPERMEMORY_API_KEY;
if (envApiKey) {
return envApiKey;
}
args: {},
handler: async (ctx): Promise<string> => {
// Check Convex environment variable first
const envApiKey = process.env.SUPERMEMORY_API_KEY
if (envApiKey) {
return envApiKey
}
// Fall back to database config
const config = await ctx.db
.query("config")
.withIndex("by_key", (q) => q.eq("key", "SUPERMEMORY_API_KEY"))
.first();
// Fall back to database config
const config = await ctx.db
.query("config")
.withIndex("by_key", (q) => q.eq("key", "SUPERMEMORY_API_KEY"))
.first()
if (config && config.value) {
return config.value as string;
}
if (config && config.value) {
return config.value as string
}
throw new Error(
"Supermemory API key not configured. Set SUPERMEMORY_API_KEY environment variable with: npx convex env set SUPERMEMORY_API_KEY your-key"
);
},
});
throw new Error(
"Supermemory API key not configured. Set SUPERMEMORY_API_KEY environment variable with: npx convex env set SUPERMEMORY_API_KEY your-key",
)
},
})

View file

@ -1,5 +1,5 @@
import { internalMutation, mutation } from "./_generated/server";
import { v } from "convex/values";
import { internalMutation, mutation } from "./_generated/server"
import { v } from "convex/values"
/**
* Supermemory Mutations
@ -13,243 +13,257 @@ import { v } from "convex/values";
* Stores search results from Supermemory API for reactive access
*/
export const cacheSearchResults = internalMutation({
args: {
query: v.string(),
containerTag: v.string(),
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(),
ttl: v.number(), // Time to live in seconds
},
handler: async (ctx, args) => {
const expiresAt = Date.now() + args.ttl * 1000;
args: {
query: v.string(),
containerTag: v.string(),
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(),
ttl: v.number(), // Time to live in seconds
},
handler: async (ctx, args) => {
const expiresAt = Date.now() + args.ttl * 1000
// Check if cache already exists
const existing = await ctx.db
.query("searchCache")
.withIndex("by_query_container", (q) =>
q.eq("query", args.query).eq("containerTag", args.containerTag)
)
.first();
// Check if cache already exists
const existing = await ctx.db
.query("searchCache")
.withIndex("by_query_container", (q) =>
q.eq("query", args.query).eq("containerTag", args.containerTag),
)
.first()
if (existing) {
// Update existing cache
await ctx.db.patch(existing._id, {
results: args.results,
timing: args.timing,
total: args.total,
expiresAt,
searchMode: args.searchMode,
});
} else {
// Create new cache entry
await ctx.db.insert("searchCache", {
query: args.query,
containerTag: args.containerTag,
searchMode: args.searchMode,
results: args.results,
timing: args.timing,
total: args.total,
expiresAt,
});
}
},
});
if (existing) {
// Update existing cache
await ctx.db.patch(existing._id, {
results: args.results,
timing: args.timing,
total: args.total,
expiresAt,
searchMode: args.searchMode,
})
} else {
// Create new cache entry
await ctx.db.insert("searchCache", {
query: args.query,
containerTag: args.containerTag,
searchMode: args.searchMode,
results: args.results,
timing: args.timing,
total: args.total,
expiresAt,
})
}
},
})
/**
* Cache user profile
* Stores user profile from Supermemory API for reactive access
*/
export const cacheProfile = internalMutation({
args: {
containerTag: v.string(),
staticProfile: v.array(v.string()),
dynamicProfile: v.array(v.string()),
searchResults: v.optional(
v.array(
v.object({
id: v.string(),
memory: v.optional(v.string()),
chunk: v.optional(v.string()),
similarity: v.number(),
metadata: v.optional(v.any()),
})
)
),
ttl: v.number(),
},
handler: async (ctx, args) => {
const expiresAt = Date.now() + args.ttl * 1000;
args: {
containerTag: v.string(),
staticProfile: v.array(v.string()),
dynamicProfile: v.array(v.string()),
searchResults: v.optional(
v.array(
v.object({
id: v.string(),
memory: v.optional(v.string()),
chunk: v.optional(v.string()),
similarity: v.number(),
metadata: v.optional(v.any()),
}),
),
),
ttl: v.number(),
},
handler: async (ctx, args) => {
const expiresAt = Date.now() + args.ttl * 1000
// Check if profile cache exists
const existing = await ctx.db
.query("profileCache")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.first();
// Check if profile cache exists
const existing = await ctx.db
.query("profileCache")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.first()
if (existing) {
// Update existing cache
await ctx.db.patch(existing._id, {
staticProfile: args.staticProfile,
dynamicProfile: args.dynamicProfile,
searchResults: args.searchResults,
expiresAt,
});
} else {
// Create new cache entry
await ctx.db.insert("profileCache", {
containerTag: args.containerTag,
staticProfile: args.staticProfile,
dynamicProfile: args.dynamicProfile,
searchResults: args.searchResults,
expiresAt,
});
}
},
});
if (existing) {
// Update existing cache
await ctx.db.patch(existing._id, {
staticProfile: args.staticProfile,
dynamicProfile: args.dynamicProfile,
searchResults: args.searchResults,
expiresAt,
})
} else {
// Create new cache entry
await ctx.db.insert("profileCache", {
containerTag: args.containerTag,
staticProfile: args.staticProfile,
dynamicProfile: args.dynamicProfile,
searchResults: args.searchResults,
expiresAt,
})
}
},
})
/**
* Store document metadata
* Tracks documents/memories added to Supermemory
*/
export const storeDocument = internalMutation({
args: {
documentId: v.string(),
customId: v.optional(v.string()),
containerTag: v.string(),
contentPreview: v.string(),
metadata: v.optional(v.any()),
status: v.union(v.literal("queued"), v.literal("processed"), v.literal("failed")),
},
handler: async (ctx, args) => {
// Check if document with this customId or documentId exists
const existingByCustomId = args.customId
? await ctx.db
.query("documents")
.withIndex("by_custom_id", (q) => q.eq("customId", args.customId))
.first()
: null;
args: {
documentId: v.string(),
customId: v.optional(v.string()),
containerTag: v.string(),
contentPreview: v.string(),
metadata: v.optional(v.any()),
status: v.union(
v.literal("queued"),
v.literal("processed"),
v.literal("failed"),
),
},
handler: async (ctx, args) => {
// Check if document with this customId or documentId exists
const existingByCustomId = args.customId
? await ctx.db
.query("documents")
.withIndex("by_custom_id", (q) => q.eq("customId", args.customId))
.first()
: null
const existingByDocId = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("documentId", args.documentId))
.first();
const existingByDocId = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("documentId", args.documentId))
.first()
const existing = existingByCustomId || existingByDocId;
const existing = existingByCustomId || existingByDocId
if (existing) {
// Update existing document
await ctx.db.patch(existing._id, {
documentId: args.documentId,
customId: args.customId,
contentPreview: args.contentPreview,
metadata: args.metadata,
status: args.status,
});
} else {
// Create new document entry
await ctx.db.insert("documents", {
documentId: args.documentId,
customId: args.customId,
containerTag: args.containerTag,
contentPreview: args.contentPreview,
metadata: args.metadata,
status: args.status,
addedAt: Date.now(),
});
}
},
});
if (existing) {
// Update existing document
await ctx.db.patch(existing._id, {
documentId: args.documentId,
customId: args.customId,
contentPreview: args.contentPreview,
metadata: args.metadata,
status: args.status,
})
} else {
// Create new document entry
await ctx.db.insert("documents", {
documentId: args.documentId,
customId: args.customId,
containerTag: args.containerTag,
contentPreview: args.contentPreview,
metadata: args.metadata,
status: args.status,
addedAt: Date.now(),
})
}
},
})
/**
* Log API call
* Records API calls for debugging and analytics
*/
export const logApiCall = internalMutation({
args: {
endpoint: v.string(),
containerTag: v.optional(v.string()),
requestBody: v.optional(v.any()),
responseStatus: v.union(
v.literal("success"),
v.literal("error"),
v.literal("pending")
),
responseTime: v.optional(v.number()),
errorMessage: v.optional(v.string()),
},
handler: async (ctx, args) => {
await ctx.db.insert("apiLogs", {
endpoint: args.endpoint,
containerTag: args.containerTag,
requestBody: args.requestBody,
responseStatus: args.responseStatus,
responseTime: args.responseTime,
errorMessage: args.errorMessage,
timestamp: Date.now(),
});
},
});
args: {
endpoint: v.string(),
containerTag: v.optional(v.string()),
requestBody: v.optional(v.any()),
responseStatus: v.union(
v.literal("success"),
v.literal("error"),
v.literal("pending"),
),
responseTime: v.optional(v.number()),
errorMessage: v.optional(v.string()),
},
handler: async (ctx, args) => {
await ctx.db.insert("apiLogs", {
endpoint: args.endpoint,
containerTag: args.containerTag,
requestBody: args.requestBody,
responseStatus: args.responseStatus,
responseTime: args.responseTime,
errorMessage: args.errorMessage,
timestamp: Date.now(),
})
},
})
/**
* Clean expired cache entries
* Removes expired search and profile caches
*/
export const cleanExpiredCache = mutation({
args: {},
handler: async (ctx) => {
const now = Date.now();
args: {},
handler: async (ctx) => {
const now = Date.now()
// Clean expired search caches
const expiredSearches = await ctx.db
.query("searchCache")
.withIndex("by_expires", (q) => q.lt("expiresAt", now))
.collect();
// Clean expired search caches
const expiredSearches = await ctx.db
.query("searchCache")
.withIndex("by_expires", (q) => q.lt("expiresAt", now))
.collect()
for (const cache of expiredSearches) {
await ctx.db.delete(cache._id);
}
for (const cache of expiredSearches) {
await ctx.db.delete(cache._id)
}
// Clean expired profile caches
const expiredProfiles = await ctx.db
.query("profileCache")
.withIndex("by_expires", (q) => q.lt("expiresAt", now))
.collect();
// Clean expired profile caches
const expiredProfiles = await ctx.db
.query("profileCache")
.withIndex("by_expires", (q) => q.lt("expiresAt", now))
.collect()
for (const cache of expiredProfiles) {
await ctx.db.delete(cache._id);
}
for (const cache of expiredProfiles) {
await ctx.db.delete(cache._id)
}
return {
cleanedSearchCaches: expiredSearches.length,
cleanedProfileCaches: expiredProfiles.length,
};
},
});
return {
cleanedSearchCaches: expiredSearches.length,
cleanedProfileCaches: expiredProfiles.length,
}
},
})
/**
* Update document status
* Updates the processing status of a document
*/
export const updateDocumentStatus = mutation({
args: {
documentId: v.string(),
status: v.union(v.literal("queued"), v.literal("processed"), v.literal("failed")),
},
handler: async (ctx, args) => {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("documentId", args.documentId))
.first();
args: {
documentId: v.string(),
status: v.union(
v.literal("queued"),
v.literal("processed"),
v.literal("failed"),
),
},
handler: async (ctx, args) => {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("documentId", args.documentId))
.first()
if (!doc) {
throw new Error(`Document ${args.documentId} not found`);
}
if (!doc) {
throw new Error(`Document ${args.documentId} not found`)
}
await ctx.db.patch(doc._id, { status: args.status });
},
});
await ctx.db.patch(doc._id, { status: args.status })
},
})
/**
* Initialize or update API key
@ -259,189 +273,199 @@ export const updateDocumentStatus = mutation({
* for production, or call this from a server-side admin endpoint with proper auth checks.
*/
export const setApiKey = internalMutation({
args: {
apiKey: v.string(),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query("config")
.withIndex("by_key", (q) => q.eq("key", "SUPERMEMORY_API_KEY"))
.first();
args: {
apiKey: v.string(),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query("config")
.withIndex("by_key", (q) => q.eq("key", "SUPERMEMORY_API_KEY"))
.first()
if (existing) {
await ctx.db.patch(existing._id, { value: args.apiKey });
} else {
await ctx.db.insert("config", {
key: "SUPERMEMORY_API_KEY",
value: args.apiKey,
});
}
},
});
if (existing) {
await ctx.db.patch(existing._id, { value: args.apiKey })
} else {
await ctx.db.insert("config", {
key: "SUPERMEMORY_API_KEY",
value: args.apiKey,
})
}
},
})
/**
* 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,
});
},
});
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;
}
}
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;
},
});
// 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;
}
}
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;
},
});
// 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();
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 (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.incrementSearches && args.responseTime) {
// Only update average when we're also incrementing searches
const totalTime = existing.avgResponseTime * existing.totalSearches;
const newTotal = totalTime + args.responseTime;
const newSearchCount = existing.totalSearches + args.incrementSearches;
updates.avgResponseTime = newTotal / newSearchCount;
}
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.incrementSearches && args.responseTime) {
// Only update average when we're also incrementing searches
const totalTime = existing.avgResponseTime * existing.totalSearches
const newTotal = totalTime + args.responseTime
const newSearchCount = existing.totalSearches + args.incrementSearches
updates.avgResponseTime = newTotal / newSearchCount
}
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(),
});
}
},
});
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

@ -1,5 +1,5 @@
import { query } from "./_generated/server";
import { v } from "convex/values";
import { query } from "./_generated/server"
import { v } from "convex/values"
/**
* Supermemory Queries
@ -13,325 +13,336 @@ import { v } from "convex/values";
* Returns cached search results if available and not expired
*/
export const getSearchCache = query({
args: {
query: v.string(),
containerTag: v.string(),
searchMode: v.optional(v.union(v.literal("hybrid"), v.literal("memories"), v.literal("documents"))),
},
handler: async (ctx, args) => {
const now = Date.now();
args: {
query: v.string(),
containerTag: v.string(),
searchMode: v.optional(
v.union(
v.literal("hybrid"),
v.literal("memories"),
v.literal("documents"),
),
),
},
handler: async (ctx, args) => {
const now = Date.now()
const cached = await ctx.db
.query("searchCache")
.withIndex("by_query_container", (q) =>
q.eq("query", args.query).eq("containerTag", args.containerTag)
)
.first();
const cached = await ctx.db
.query("searchCache")
.withIndex("by_query_container", (q) =>
q.eq("query", args.query).eq("containerTag", args.containerTag),
)
.first()
// Return null if cache expired or searchMode doesn't match
if (!cached || cached.expiresAt < now) {
return null;
}
if (args.searchMode && cached.searchMode !== args.searchMode) {
return null;
}
// Return null if cache expired or searchMode doesn't match
if (!cached || cached.expiresAt < now) {
return null
}
if (args.searchMode && cached.searchMode !== args.searchMode) {
return null
}
return cached;
},
});
return cached
},
})
/**
* Get cached user profile
* Returns cached profile if available and not expired
*/
export const getProfileCache = query({
args: {
containerTag: v.string(),
},
handler: async (ctx, args) => {
const now = Date.now();
args: {
containerTag: v.string(),
},
handler: async (ctx, args) => {
const now = Date.now()
const cached = await ctx.db
.query("profileCache")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.first();
const cached = await ctx.db
.query("profileCache")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.first()
// Return null if cache expired
if (!cached || cached.expiresAt < now) {
return null;
}
// Return null if cache expired
if (!cached || cached.expiresAt < now) {
return null
}
return cached;
},
});
return cached
},
})
/**
* List documents added to Supermemory
* Provides visibility into what content has been indexed
*/
export const listDocuments = query({
args: {
containerTag: v.optional(v.string()),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit || 50;
args: {
containerTag: v.optional(v.string()),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit || 50
if (args.containerTag) {
return await ctx.db
.query("documents")
.withIndex("by_container", (q) =>
q.eq("containerTag", args.containerTag)
)
.order("desc")
.take(limit);
}
if (args.containerTag) {
return await ctx.db
.query("documents")
.withIndex("by_container", (q) =>
q.eq("containerTag", args.containerTag),
)
.order("desc")
.take(limit)
}
return await ctx.db.query("documents").order("desc").take(limit);
},
});
return await ctx.db.query("documents").order("desc").take(limit)
},
})
/**
* Get document by custom ID
* Find a specific document using your custom identifier
*/
export const getDocumentByCustomId = query({
args: {
customId: v.string(),
},
handler: async (ctx, args) => {
return await ctx.db
.query("documents")
.withIndex("by_custom_id", (q) => q.eq("customId", args.customId))
.first();
},
});
args: {
customId: v.string(),
},
handler: async (ctx, args) => {
return await ctx.db
.query("documents")
.withIndex("by_custom_id", (q) => q.eq("customId", args.customId))
.first()
},
})
/**
* Get API call logs
* View recent Supermemory API calls for debugging and analytics
*/
export const getApiLogs = query({
args: {
endpoint: v.optional(v.string()),
containerTag: v.optional(v.string()),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit || 100;
args: {
endpoint: v.optional(v.string()),
containerTag: v.optional(v.string()),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit || 100
if (args.endpoint) {
return await ctx.db
.query("apiLogs")
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint))
.order("desc")
.take(limit);
}
if (args.endpoint) {
return await ctx.db
.query("apiLogs")
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint))
.order("desc")
.take(limit)
}
if (args.containerTag) {
return await ctx.db
.query("apiLogs")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.order("desc")
.take(limit);
}
if (args.containerTag) {
return await ctx.db
.query("apiLogs")
.withIndex("by_container", (q) =>
q.eq("containerTag", args.containerTag),
)
.order("desc")
.take(limit)
}
return await ctx.db.query("apiLogs").order("desc").take(limit);
},
});
return await ctx.db.query("apiLogs").order("desc").take(limit)
},
})
/**
* Get API statistics
* Aggregate stats for dashboard visibility
*/
export const getApiStats = query({
args: {
containerTag: v.optional(v.string()),
},
handler: async (ctx, args) => {
const logs = args.containerTag
? await ctx.db
.query("apiLogs")
.withIndex("by_container", (q) =>
q.eq("containerTag", args.containerTag)
)
.collect()
: await ctx.db.query("apiLogs").collect();
args: {
containerTag: v.optional(v.string()),
},
handler: async (ctx, args) => {
const logs = args.containerTag
? await ctx.db
.query("apiLogs")
.withIndex("by_container", (q) =>
q.eq("containerTag", args.containerTag),
)
.collect()
: await ctx.db.query("apiLogs").collect()
const stats = {
totalCalls: logs.length,
successfulCalls: logs.filter((l) => l.responseStatus === "success").length,
failedCalls: logs.filter((l) => l.responseStatus === "error").length,
averageResponseTime:
logs.reduce((sum, l) => sum + (l.responseTime || 0), 0) / logs.length ||
0,
callsByEndpoint: {} as Record<string, number>,
};
const stats = {
totalCalls: logs.length,
successfulCalls: logs.filter((l) => l.responseStatus === "success")
.length,
failedCalls: logs.filter((l) => l.responseStatus === "error").length,
averageResponseTime:
logs.reduce((sum, l) => sum + (l.responseTime || 0), 0) / logs.length ||
0,
callsByEndpoint: {} as Record<string, number>,
}
// Count calls by endpoint
for (const log of logs) {
stats.callsByEndpoint[log.endpoint] =
(stats.callsByEndpoint[log.endpoint] || 0) + 1;
}
// Count calls by endpoint
for (const log of logs) {
stats.callsByEndpoint[log.endpoint] =
(stats.callsByEndpoint[log.endpoint] || 0) + 1
}
return stats;
},
});
return stats
},
})
/**
* Search documents locally (in Convex cache)
* Fast text search across cached content previews
*/
export const searchCachedDocuments = query({
args: {
searchText: v.string(),
containerTag: v.optional(v.string()),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit || 20;
const searchLower = args.searchText.toLowerCase();
args: {
searchText: v.string(),
containerTag: v.optional(v.string()),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit || 20
const searchLower = args.searchText.toLowerCase()
const query = args.containerTag
? ctx.db
.query("documents")
.withIndex("by_container", (q) =>
q.eq("containerTag", args.containerTag)
)
: ctx.db.query("documents");
const query = args.containerTag
? ctx.db
.query("documents")
.withIndex("by_container", (q) =>
q.eq("containerTag", args.containerTag),
)
: ctx.db.query("documents")
const allDocs = await query.collect();
const allDocs = await query.collect()
// Simple text matching on content preview
return allDocs
.filter((doc) => doc.contentPreview.toLowerCase().includes(searchLower))
.slice(0, limit);
},
});
// Simple text matching on content preview
return allDocs
.filter((doc) => doc.contentPreview.toLowerCase().includes(searchLower))
.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;
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_container", (q) =>
q.eq("source", args.source).eq("containerTag", args.containerTag)
)
.order("desc")
.take(limit);
}
if (args.source) {
return await ctx.db
.query("memories")
.withIndex("by_source_container", (q) =>
q.eq("source", args.source).eq("containerTag", args.containerTag),
)
.order("desc")
.take(limit)
}
return await ctx.db
.query("memories")
.withIndex("by_container", (q) => q.eq("containerTag", args.containerTag))
.order("desc")
.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;
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);
},
});
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);
},
});
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();
},
});
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();
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 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 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);
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,
};
},
});
return {
analytics: analytics || {
totalMemories: 0,
totalChats: 0,
totalSearches: 0,
avgResponseTime: 0,
lastActive: Date.now(),
},
recentMemories,
recentSessions,
recentDocuments,
}
},
})

View file

@ -1,5 +1,5 @@
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { defineSchema, defineTable } from "convex/server"
import { v } from "convex/values"
/**
* Convex schema for Supermemory component
@ -8,142 +8,156 @@ import { v } from "convex/values";
* enabling reactive queries and reducing API calls.
*/
export default defineSchema({
/**
* Cached search results from Supermemory
* Stores recent search queries and their results for fast reactive access
*/
searchCache: defineTable({
query: v.string(),
containerTag: v.string(),
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(),
expiresAt: v.number(), // Timestamp when cache expires
})
.index("by_query_container", ["query", "containerTag"])
.index("by_expires", ["expiresAt"]),
/**
* Cached search results from Supermemory
* Stores recent search queries and their results for fast reactive access
*/
searchCache: defineTable({
query: v.string(),
containerTag: v.string(),
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(),
expiresAt: v.number(), // Timestamp when cache expires
})
.index("by_query_container", ["query", "containerTag"])
.index("by_expires", ["expiresAt"]),
/**
* Cached user profiles from Supermemory
* Stores user context (static + dynamic facts) for fast access
*/
profileCache: defineTable({
containerTag: v.string(),
staticProfile: v.array(v.string()),
dynamicProfile: v.array(v.string()),
searchResults: v.optional(
v.array(
v.object({
id: v.string(),
memory: v.optional(v.string()),
chunk: v.optional(v.string()),
similarity: v.number(),
metadata: v.optional(v.any()),
})
)
),
expiresAt: v.number(),
})
.index("by_container", ["containerTag"])
.index("by_expires", ["expiresAt"]),
/**
* Cached user profiles from Supermemory
* Stores user context (static + dynamic facts) for fast access
*/
profileCache: defineTable({
containerTag: v.string(),
staticProfile: v.array(v.string()),
dynamicProfile: v.array(v.string()),
searchResults: v.optional(
v.array(
v.object({
id: v.string(),
memory: v.optional(v.string()),
chunk: v.optional(v.string()),
similarity: v.number(),
metadata: v.optional(v.any()),
}),
),
),
expiresAt: v.number(),
})
.index("by_container", ["containerTag"])
.index("by_expires", ["expiresAt"]),
/**
* Metadata about documents/memories added to Supermemory
* Tracks what content has been sent to Supermemory for analytics
*/
documents: defineTable({
documentId: v.string(), // Supermemory document ID
customId: v.optional(v.string()),
containerTag: v.string(),
contentPreview: v.string(), // First 200 chars for reference
metadata: v.optional(v.any()),
status: v.union(v.literal("queued"), v.literal("processed"), v.literal("failed")),
addedAt: v.number(),
})
.index("by_container", ["containerTag"])
.index("by_custom_id", ["customId"])
.index("by_document_id", ["documentId"])
.index("by_status", ["status"]),
/**
* Metadata about documents/memories added to Supermemory
* Tracks what content has been sent to Supermemory for analytics
*/
documents: defineTable({
documentId: v.string(), // Supermemory document ID
customId: v.optional(v.string()),
containerTag: v.string(),
contentPreview: v.string(), // First 200 chars for reference
metadata: v.optional(v.any()),
status: v.union(
v.literal("queued"),
v.literal("processed"),
v.literal("failed"),
),
addedAt: v.number(),
})
.index("by_container", ["containerTag"])
.index("by_custom_id", ["customId"])
.index("by_document_id", ["documentId"])
.index("by_status", ["status"]),
/**
* API call logs for dashboard visibility
* Tracks all Supermemory API calls for debugging and analytics
*/
apiLogs: defineTable({
endpoint: v.string(), // "add", "search", "profile", etc.
containerTag: v.optional(v.string()),
requestBody: v.optional(v.any()),
responseStatus: v.union(
v.literal("success"),
v.literal("error"),
v.literal("pending")
),
responseTime: v.optional(v.number()), // milliseconds
errorMessage: v.optional(v.string()),
timestamp: v.number(),
})
.index("by_endpoint", ["endpoint"])
.index("by_container", ["containerTag"])
.index("by_timestamp", ["timestamp"])
.index("by_status", ["responseStatus"]),
/**
* API call logs for dashboard visibility
* Tracks all Supermemory API calls for debugging and analytics
*/
apiLogs: defineTable({
endpoint: v.string(), // "add", "search", "profile", etc.
containerTag: v.optional(v.string()),
requestBody: v.optional(v.any()),
responseStatus: v.union(
v.literal("success"),
v.literal("error"),
v.literal("pending"),
),
responseTime: v.optional(v.number()), // milliseconds
errorMessage: v.optional(v.string()),
timestamp: v.number(),
})
.index("by_endpoint", ["endpoint"])
.index("by_container", ["containerTag"])
.index("by_timestamp", ["timestamp"])
.index("by_status", ["responseStatus"]),
/**
* Component configuration
* Stores API key and other settings
*/
config: defineTable({
key: v.string(),
value: v.any(),
}).index("by_key", ["key"]),
/**
* Component configuration
* Stores API key and other settings
*/
config: defineTable({
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_source_container", ["source", "containerTag"])
.index("by_created", ["createdAt"]),
/**
* 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_source_container", ["source", "containerTag"])
.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"]),
/**
* 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"]),
});
/**
* 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

@ -1,16 +1,16 @@
import { useAction, useQuery, useMutation } from "convex/react";
import { useState, useCallback } from "react";
import type { FunctionReference } from "convex/server";
import { useAction, useQuery, useMutation } from "convex/react"
import { useState, useCallback } from "react"
import type { FunctionReference } from "convex/server"
import type {
AddMemoryArgs,
SearchMemoriesArgs,
ProfileArgs,
SearchResponse,
ProfileResponse,
Document,
ApiLog,
ApiStats,
} from "../client/index";
AddMemoryArgs,
SearchMemoriesArgs,
ProfileArgs,
SearchResponse,
ProfileResponse,
Document,
ApiLog,
ApiStats,
} from "../client/index"
/**
* React Hooks for Supermemory Convex Component
@ -38,16 +38,17 @@ import type {
* }
* ```
*/
export function useAddMemory(componentPath: string = "supermemory") {
const action = `${componentPath}:actions.add` as unknown as FunctionReference<"action">;
const addAction = useAction(action);
export function useAddMemory(componentPath = "supermemory") {
const action =
`${componentPath}:actions.add` as unknown as FunctionReference<"action">
const addAction = useAction(action)
return useCallback(
async (args: AddMemoryArgs) => {
return await addAction(args);
},
[addAction]
);
return useCallback(
async (args: AddMemoryArgs) => {
return await addAction(args)
},
[addAction],
)
}
/**
@ -79,41 +80,42 @@ export function useAddMemory(componentPath: string = "supermemory") {
* ```
*/
export function useSupermemorySearch(
args: SearchMemoriesArgs | null,
componentPath: string = "supermemory"
args: SearchMemoriesArgs | null,
componentPath = "supermemory",
) {
const action = `${componentPath}:actions.search` as unknown as FunctionReference<"action">;
const searchAction = useAction(action);
const [results, setResults] = useState<SearchResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const action =
`${componentPath}:actions.search` as unknown as FunctionReference<"action">
const searchAction = useAction(action)
const [results, setResults] = useState<SearchResponse | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const search = useCallback(
async (searchArgs?: SearchMemoriesArgs) => {
const finalArgs = searchArgs || args;
if (!finalArgs) return;
const search = useCallback(
async (searchArgs?: SearchMemoriesArgs) => {
const finalArgs = searchArgs || args
if (!finalArgs) return
setIsLoading(true);
setError(null);
setIsLoading(true)
setError(null)
try {
const response = await searchAction(finalArgs);
setResults(response as SearchResponse);
} catch (err) {
setError(err instanceof Error ? err : new Error("Search failed"));
} finally {
setIsLoading(false);
}
},
[searchAction, args]
);
try {
const response = await searchAction(finalArgs)
setResults(response as SearchResponse)
} catch (err) {
setError(err instanceof Error ? err : new Error("Search failed"))
} finally {
setIsLoading(false)
}
},
[searchAction, args],
)
return {
results,
isLoading,
error,
search,
};
return {
results,
isLoading,
error,
search,
}
}
/**
@ -145,41 +147,42 @@ export function useSupermemorySearch(
* ```
*/
export function useSupermemoryProfile(
args: ProfileArgs | null,
componentPath: string = "supermemory"
args: ProfileArgs | null,
componentPath = "supermemory",
) {
const action = `${componentPath}:actions.profile` as unknown as FunctionReference<"action">;
const profileAction = useAction(action);
const [profile, setProfile] = useState<ProfileResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const action =
`${componentPath}:actions.profile` as unknown as FunctionReference<"action">
const profileAction = useAction(action)
const [profile, setProfile] = useState<ProfileResponse | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const refresh = useCallback(
async (profileArgs?: ProfileArgs) => {
const finalArgs = profileArgs || args;
if (!finalArgs) return;
const refresh = useCallback(
async (profileArgs?: ProfileArgs) => {
const finalArgs = profileArgs || args
if (!finalArgs) return
setIsLoading(true);
setError(null);
setIsLoading(true)
setError(null)
try {
const response = await profileAction(finalArgs);
setProfile(response as ProfileResponse);
} catch (err) {
setError(err instanceof Error ? err : new Error("Profile fetch failed"));
} finally {
setIsLoading(false);
}
},
[profileAction, args]
);
try {
const response = await profileAction(finalArgs)
setProfile(response as ProfileResponse)
} catch (err) {
setError(err instanceof Error ? err : new Error("Profile fetch failed"))
} finally {
setIsLoading(false)
}
},
[profileAction, args],
)
return {
profile,
isLoading,
error,
refresh,
};
return {
profile,
isLoading,
error,
refresh,
}
}
/**
@ -207,11 +210,12 @@ export function useSupermemoryProfile(
* ```
*/
export function useDocumentList(
args?: { containerTag?: string; limit?: number },
componentPath: string = "supermemory"
args?: { containerTag?: string; limit?: number },
componentPath = "supermemory",
) {
const query = `${componentPath}:queries.listDocuments` as unknown as FunctionReference<"query">;
return useQuery(query, args || {}) as Document[] | undefined;
const query =
`${componentPath}:queries.listDocuments` as unknown as FunctionReference<"query">
return useQuery(query, args || {}) as Document[] | undefined
}
/**
@ -221,15 +225,15 @@ export function useDocumentList(
* @param componentPath - Path to the component (default: "supermemory")
*/
export function useDocument(
customId: string | null,
componentPath: string = "supermemory"
customId: string | null,
componentPath = "supermemory",
) {
const query =
`${componentPath}:queries.getDocumentByCustomId` as unknown as FunctionReference<"query">;
return useQuery(
query,
customId ? { customId } : "skip"
) as Document | null | undefined;
const query =
`${componentPath}:queries.getDocumentByCustomId` as unknown as FunctionReference<"query">
return useQuery(query, customId ? { customId } : "skip") as
| Document
| null
| undefined
}
/**
@ -256,11 +260,12 @@ export function useDocument(
* ```
*/
export function useApiLogs(
args?: { endpoint?: string; containerTag?: string; limit?: number },
componentPath: string = "supermemory"
args?: { endpoint?: string; containerTag?: string; limit?: number },
componentPath = "supermemory",
) {
const query = `${componentPath}:queries.getApiLogs` as unknown as FunctionReference<"query">;
return useQuery(query, args || {}) as ApiLog[] | undefined;
const query =
`${componentPath}:queries.getApiLogs` as unknown as FunctionReference<"query">
return useQuery(query, args || {}) as ApiLog[] | undefined
}
/**
@ -285,11 +290,12 @@ export function useApiLogs(
* ```
*/
export function useApiStats(
args?: { containerTag?: string },
componentPath: string = "supermemory"
args?: { containerTag?: string },
componentPath = "supermemory",
) {
const query = `${componentPath}:queries.getApiStats` as unknown as FunctionReference<"query">;
return useQuery(query, args || {}) as ApiStats | undefined;
const query =
`${componentPath}:queries.getApiStats` as unknown as FunctionReference<"query">
return useQuery(query, args || {}) as ApiStats | undefined
}
/**
@ -297,14 +303,14 @@ export function useApiStats(
*
* @param componentPath - Path to the component (default: "supermemory")
*/
export function useCleanCache(componentPath: string = "supermemory") {
const mutation =
`${componentPath}:mutations.cleanExpiredCache` as unknown as FunctionReference<"mutation">;
const cleanMutation = useMutation(mutation);
export function useCleanCache(componentPath = "supermemory") {
const mutation =
`${componentPath}:mutations.cleanExpiredCache` as unknown as FunctionReference<"mutation">
const cleanMutation = useMutation(mutation)
return useCallback(async () => {
return await cleanMutation({});
}, [cleanMutation]);
return useCallback(async () => {
return await cleanMutation({})
}, [cleanMutation])
}
/**
@ -312,17 +318,20 @@ export function useCleanCache(componentPath: string = "supermemory") {
*
* @param componentPath - Path to the component (default: "supermemory")
*/
export function useUpdateDocumentStatus(componentPath: string = "supermemory") {
const mutation =
`${componentPath}:mutations.updateDocumentStatus` as unknown as FunctionReference<"mutation">;
const updateMutation = useMutation(mutation);
export function useUpdateDocumentStatus(componentPath = "supermemory") {
const mutation =
`${componentPath}:mutations.updateDocumentStatus` as unknown as FunctionReference<"mutation">
const updateMutation = useMutation(mutation)
return useCallback(
async (args: { documentId: string; status: "queued" | "processed" | "failed" }) => {
return await updateMutation(args);
},
[updateMutation]
);
return useCallback(
async (args: {
documentId: string
status: "queued" | "processed" | "failed"
}) => {
return await updateMutation(args)
},
[updateMutation],
)
}
/**
@ -350,11 +359,16 @@ export function useUpdateDocumentStatus(componentPath: string = "supermemory") {
* ```
*/
export function useMemories(
args: { containerTag: string; source?: "chat" | "document" | "manual"; limit?: number },
componentPath: string = "supermemory"
args: {
containerTag: string
source?: "chat" | "document" | "manual"
limit?: number
},
componentPath = "supermemory",
) {
const query = `${componentPath}:queries.listMemories` as unknown as FunctionReference<"query">;
return useQuery(query, args) as any[] | undefined;
const query =
`${componentPath}:queries.listMemories` as unknown as FunctionReference<"query">
return useQuery(query, args) as any[] | undefined
}
/**
@ -382,11 +396,12 @@ export function useMemories(
* ```
*/
export function useChatSessions(
args: { containerTag: string; limit?: number },
componentPath: string = "supermemory"
args: { containerTag: string; limit?: number },
componentPath = "supermemory",
) {
const query = `${componentPath}:queries.getChatSessions` as unknown as FunctionReference<"query">;
return useQuery(query, args) as any[] | undefined;
const query =
`${componentPath}:queries.getChatSessions` as unknown as FunctionReference<"query">
return useQuery(query, args) as any[] | undefined
}
/**
@ -396,11 +411,15 @@ export function useChatSessions(
* @param componentPath - Path to the component (default: "supermemory")
*/
export function useChatSession(
sessionId: string | null,
componentPath: string = "supermemory"
sessionId: string | null,
componentPath = "supermemory",
) {
const query = `${componentPath}:queries.getChatSession` as unknown as FunctionReference<"query">;
return useQuery(query, sessionId ? { sessionId } : "skip") as any | null | undefined;
const query =
`${componentPath}:queries.getChatSession` as unknown as FunctionReference<"query">
return useQuery(query, sessionId ? { sessionId } : "skip") as
| any
| null
| undefined
}
/**
@ -428,11 +447,15 @@ export function useChatSession(
* ```
*/
export function useAnalytics(
containerTag: string | null,
componentPath: string = "supermemory"
containerTag: string | null,
componentPath = "supermemory",
) {
const query = `${componentPath}:queries.getAnalytics` as unknown as FunctionReference<"query">;
return useQuery(query, containerTag ? { containerTag } : "skip") as any | null | undefined;
const query =
`${componentPath}:queries.getAnalytics` as unknown as FunctionReference<"query">
return useQuery(query, containerTag ? { containerTag } : "skip") as
| any
| null
| undefined
}
/**
@ -467,21 +490,24 @@ export function useAnalytics(
* ```
*/
export function useDashboardOverview(
containerTag: string | null,
componentPath: string = "supermemory"
containerTag: string | null,
componentPath = "supermemory",
) {
const query = `${componentPath}:queries.getDashboardOverview` as unknown as FunctionReference<"query">;
return useQuery(query, containerTag ? { containerTag } : "skip") as any | undefined;
const query =
`${componentPath}:queries.getDashboardOverview` as unknown as FunctionReference<"query">
return useQuery(query, containerTag ? { containerTag } : "skip") as
| any
| undefined
}
// Export all types
export type {
AddMemoryArgs,
SearchMemoriesArgs,
ProfileArgs,
SearchResponse,
ProfileResponse,
Document,
ApiLog,
ApiStats,
} from "../client/index";
AddMemoryArgs,
SearchMemoriesArgs,
ProfileArgs,
SearchResponse,
ProfileResponse,
Document,
ApiLog,
ApiStats,
} from "../client/index"

View file

@ -1,23 +1,23 @@
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": false,
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"types": ["react"],
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/client/**/*", "src/react/**/*", "src/ai-sdk/**/*"],
"exclude": ["node_modules", "dist", "src/component"]
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": false,
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"types": ["react"],
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/client/**/*", "src/react/**/*", "src/ai-sdk/**/*"],
"exclude": ["node_modules", "dist", "src/component"]
}