mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-19 00:01:34 +00:00
416 lines
9.7 KiB
TypeScript
416 lines
9.7 KiB
TypeScript
import Supermemory from "supermemory"
|
|
|
|
const MAX_CHARS = 200000 // ~50k tokens (character-based limit)
|
|
const DEFAULT_PROJECT_ID = "sm_project_default"
|
|
|
|
export type Memory =
|
|
| {
|
|
id: string
|
|
memory: string
|
|
similarity: number
|
|
title?: string
|
|
content?: string
|
|
}
|
|
| {
|
|
id: string
|
|
chunk: string
|
|
similarity: number
|
|
title?: string
|
|
content?: string
|
|
}
|
|
|
|
export interface SearchResult {
|
|
results: Memory[]
|
|
total: number
|
|
timing: number
|
|
}
|
|
|
|
export interface Profile {
|
|
static: string[]
|
|
dynamic: string[]
|
|
}
|
|
|
|
export interface ProfileResponse {
|
|
profile: Profile
|
|
searchResults?: SearchResult
|
|
}
|
|
|
|
export interface Project {
|
|
id: string
|
|
name: string
|
|
containerTag: string
|
|
createdAt: string
|
|
updatedAt: string
|
|
isExperimental: boolean
|
|
documentCount?: number
|
|
}
|
|
|
|
// Documents API types
|
|
export interface DocumentMemoryEntry {
|
|
id: string
|
|
memory: string
|
|
spaceId: string
|
|
isStatic?: boolean
|
|
isLatest?: boolean
|
|
isForgotten?: boolean
|
|
forgetAfter?: string | null
|
|
forgetReason?: string | null
|
|
version?: number
|
|
parentMemoryId?: string | null
|
|
rootMemoryId?: string | null
|
|
createdAt: string
|
|
updatedAt: string
|
|
}
|
|
|
|
export interface DocumentWithMemories {
|
|
id: string
|
|
title: string | null
|
|
summary?: string | null
|
|
type: string
|
|
createdAt: string
|
|
updatedAt: string
|
|
memoryEntries: DocumentMemoryEntry[]
|
|
}
|
|
|
|
export interface DocumentsApiResponse {
|
|
documents: DocumentWithMemories[]
|
|
pagination: {
|
|
currentPage: number
|
|
limit: number
|
|
totalItems: number
|
|
totalPages: number
|
|
}
|
|
}
|
|
|
|
export function getMemoryText(m: Memory): string {
|
|
return "memory" in m ? m.memory : m.chunk
|
|
}
|
|
|
|
function limitByChars(text: string, maxChars = MAX_CHARS): string {
|
|
return text.length > maxChars ? `${text.slice(0, maxChars)}...` : text
|
|
}
|
|
|
|
// Type for SDK search result item
|
|
interface SDKResult {
|
|
id: string
|
|
memory?: string
|
|
chunk?: string
|
|
content?: string
|
|
similarity: number
|
|
title?: string
|
|
context?: string
|
|
}
|
|
|
|
export class SupermemoryClient {
|
|
private client: Supermemory
|
|
private containerTag: string
|
|
private bearerToken: string
|
|
private apiUrl: string
|
|
|
|
constructor(
|
|
bearerToken: string,
|
|
containerTag?: string,
|
|
apiUrl = "https://api.supermemory.ai",
|
|
) {
|
|
this.bearerToken = bearerToken
|
|
this.apiUrl = apiUrl
|
|
this.client = new Supermemory({
|
|
apiKey: bearerToken,
|
|
baseURL: apiUrl,
|
|
})
|
|
this.containerTag = containerTag || DEFAULT_PROJECT_ID
|
|
}
|
|
|
|
// Create memory using SDK
|
|
async createMemory(
|
|
content: string,
|
|
): Promise<{ id: string; status: string; containerTag: string }> {
|
|
try {
|
|
const result = await this.client.add({
|
|
content,
|
|
containerTag: this.containerTag,
|
|
metadata: {
|
|
sm_source: "mcp",
|
|
},
|
|
})
|
|
return {
|
|
id: result.id,
|
|
status: "queued",
|
|
containerTag: this.containerTag,
|
|
}
|
|
} catch (error) {
|
|
this.handleError(error)
|
|
}
|
|
}
|
|
|
|
// Delete/forget memory - try exact match first, then semantic search
|
|
async forgetMemory(
|
|
content: string,
|
|
): Promise<{ success: boolean; message: string; containerTag: string }> {
|
|
try {
|
|
// Try exact content matching first
|
|
try {
|
|
const result = await this.client.memories.forget({
|
|
content: content,
|
|
containerTag: this.containerTag,
|
|
})
|
|
|
|
return {
|
|
success: true,
|
|
message: `Successfully forgot memory (exact match) with ID: ${result.id}`,
|
|
containerTag: this.containerTag,
|
|
}
|
|
} catch (error: unknown) {
|
|
// If not 404, it's a real error - re-throw it
|
|
const status =
|
|
error && typeof error === "object" && "status" in error
|
|
? (error as Record<string, unknown>).status
|
|
: undefined
|
|
if (status !== 404) {
|
|
throw error
|
|
}
|
|
// Otherwise continue to semantic search fallback
|
|
}
|
|
|
|
// Fallback to semantic search if exact match fails
|
|
const SIMILARITY_THRESHOLD = 0.85 // High threshold - only very similar memories
|
|
const searchResult = await this.search(content, 5, SIMILARITY_THRESHOLD)
|
|
|
|
if (searchResult.results.length === 0) {
|
|
return {
|
|
success: false,
|
|
message: `No matching memory found to forget. Tried exact match and semantic search with similarity threshold ${SIMILARITY_THRESHOLD}.`,
|
|
containerTag: this.containerTag,
|
|
}
|
|
}
|
|
|
|
// Only actual memories (not chunks) can be forgotten
|
|
const memoryToDelete = searchResult.results.find((r) => "memory" in r)
|
|
if (!memoryToDelete) {
|
|
return {
|
|
success: false,
|
|
message:
|
|
"No matching memory found to forget (only document chunks matched in semantic search).",
|
|
containerTag: this.containerTag,
|
|
}
|
|
}
|
|
|
|
// Delete using the ID from semantic search
|
|
await this.client.memories.forget({
|
|
id: memoryToDelete.id,
|
|
containerTag: this.containerTag,
|
|
})
|
|
|
|
const memoryText =
|
|
getMemoryText(memoryToDelete) || memoryToDelete.content || ""
|
|
return {
|
|
success: true,
|
|
message: `Forgot similar memory (semantic match, similarity: ${memoryToDelete.similarity.toFixed(2)}): "${limitByChars(memoryText, 100)}"`,
|
|
containerTag: this.containerTag,
|
|
}
|
|
} catch (error) {
|
|
this.handleError(error)
|
|
}
|
|
}
|
|
|
|
// Search memories using SDK
|
|
async search(
|
|
query: string,
|
|
limit = 10,
|
|
threshold?: number,
|
|
): Promise<SearchResult> {
|
|
try {
|
|
const result = await this.client.search.memories({
|
|
q: query,
|
|
limit,
|
|
containerTag: this.containerTag,
|
|
searchMode: "hybrid",
|
|
threshold, // Optional threshold parameter
|
|
})
|
|
|
|
// Normalize and limit response size — preserve memory vs chunk distinction
|
|
const results: Memory[] = (result.results as SDKResult[]).map((r) => {
|
|
const text = limitByChars(
|
|
r.content || r.memory || r.chunk || r.context || "",
|
|
)
|
|
const base = {
|
|
id: r.id,
|
|
similarity: r.similarity,
|
|
title: r.title,
|
|
content: r.content,
|
|
}
|
|
if (r.chunk && !r.memory) {
|
|
return { ...base, chunk: text }
|
|
}
|
|
return { ...base, memory: text }
|
|
})
|
|
|
|
return {
|
|
results,
|
|
total: result.total,
|
|
timing: result.timing,
|
|
}
|
|
} catch (error) {
|
|
this.handleError(error)
|
|
}
|
|
}
|
|
|
|
// Get user profile using SDK
|
|
async getProfile(query?: string): Promise<ProfileResponse> {
|
|
try {
|
|
const result = await this.client.profile({
|
|
containerTag: this.containerTag,
|
|
q: query,
|
|
})
|
|
|
|
const response: ProfileResponse = {
|
|
profile: {
|
|
static: result.profile?.static || [],
|
|
dynamic: result.profile?.dynamic || [],
|
|
},
|
|
}
|
|
|
|
if (result.searchResults) {
|
|
response.searchResults = {
|
|
results: (result.searchResults.results as SDKResult[]).map((r) => {
|
|
const text = limitByChars(
|
|
r.content || r.memory || r.chunk || r.context || "",
|
|
)
|
|
const base = {
|
|
id: r.id,
|
|
similarity: r.similarity,
|
|
title: r.title,
|
|
content: r.content,
|
|
}
|
|
if (r.chunk && !r.memory) {
|
|
return { ...base, chunk: text }
|
|
}
|
|
return { ...base, memory: text }
|
|
}),
|
|
total: result.searchResults.total,
|
|
timing: result.searchResults.timing,
|
|
}
|
|
}
|
|
|
|
return response
|
|
} catch (error) {
|
|
this.handleError(error)
|
|
}
|
|
}
|
|
|
|
// Get projects list
|
|
async getProjects(): Promise<string[]> {
|
|
try {
|
|
const response = await fetch(`${this.apiUrl}/v3/projects`, {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: `Bearer ${this.bearerToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
throw new Error("Authentication failed. Please re-authenticate.")
|
|
}
|
|
throw new Error(`Failed to fetch projects: ${response.statusText}`)
|
|
}
|
|
|
|
const data = (await response.json()) as {
|
|
projects: Project[]
|
|
}
|
|
return data.projects?.map((p) => p.containerTag) || []
|
|
} catch (error) {
|
|
this.handleError(error)
|
|
}
|
|
}
|
|
|
|
// Fetch documents with their memory entries
|
|
async getDocuments(
|
|
containerTags?: string[],
|
|
page = 1,
|
|
limit = 10,
|
|
): Promise<DocumentsApiResponse> {
|
|
try {
|
|
const response = await fetch(`${this.apiUrl}/v3/documents/documents`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${this.bearerToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
page,
|
|
limit,
|
|
sort: "createdAt",
|
|
order: "desc",
|
|
containerTags,
|
|
}),
|
|
})
|
|
if (!response.ok) {
|
|
throw Object.assign(new Error("Failed to fetch documents"), {
|
|
status: response.status,
|
|
})
|
|
}
|
|
return (await response.json()) as DocumentsApiResponse
|
|
} catch (error) {
|
|
this.handleError(error)
|
|
}
|
|
}
|
|
|
|
private handleError(error: unknown): never {
|
|
// Handle network/fetch errors
|
|
if (error instanceof TypeError) {
|
|
if (
|
|
error.message.includes("fetch") ||
|
|
error.message.includes("network")
|
|
) {
|
|
throw new Error(
|
|
"Network error. Please check your connection and try again.",
|
|
)
|
|
}
|
|
}
|
|
|
|
// Handle HTTP status errors from SDK/fetch
|
|
if (error && typeof error === "object" && "status" in error) {
|
|
const status = (error as { status: number }).status
|
|
const message =
|
|
"message" in error ? (error as { message: string }).message : undefined
|
|
|
|
switch (status) {
|
|
case 400:
|
|
case 422:
|
|
throw new Error(
|
|
message || "Invalid request parameters. Please check your input.",
|
|
)
|
|
case 401:
|
|
throw new Error("Authentication failed. Please re-authenticate.")
|
|
case 402:
|
|
throw new Error("Memory limit reached. Upgrade at supermemory.ai")
|
|
case 403:
|
|
throw new Error(
|
|
"Access forbidden. Your account may be restricted or blocked.",
|
|
)
|
|
case 404:
|
|
throw new Error("Memory not found. It may have been deleted.")
|
|
case 429:
|
|
throw new Error(
|
|
"Rate limit exceeded. Please wait a moment and try again.",
|
|
)
|
|
default:
|
|
if (status >= 500) {
|
|
throw new Error(
|
|
"Server error. The service may be temporarily unavailable. Please try again later.",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Re-throw Error instances as-is
|
|
if (error instanceof Error) {
|
|
throw error
|
|
}
|
|
|
|
// Wrap unknown errors
|
|
throw new Error(`An unexpected error occurred: ${String(error)}`)
|
|
}
|
|
}
|