()
+ for (const entry of directoryEntries) {
+ for (const category of entry.categories) {
+ counts.set(category, (counts.get(category) ?? 0) + 1)
+ }
+ }
+ return [...counts.entries()]
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10)
+ .map(([category]) => category)
+ }, [directoryEntries])
+
+ const marketplaceEntries = useMemo(
+ () =>
+ marketplaceCategory === "all"
+ ? directoryEntries
+ : directoryEntries.filter((entry) =>
+ entry.categories.includes(marketplaceCategory),
+ ),
+ [directoryEntries, marketplaceCategory],
+ )
+
+ const needle = query.trim().toLowerCase()
+ const searching = needle.length > 0
+ const catalogMatches = searching
+ ? apps.filter(
+ (entry) =>
+ entry.name.toLowerCase().includes(needle) ||
+ entry.category.toLowerCase().includes(needle),
+ )
+ : []
+ const slackMatches = searching && "slack messaging".includes(needle)
+
if (!isCompanyBrain) {
return (
entry.slug))
- const canClassifyCustomRows = catalogLoaded && apps.length > 0
- const customRows = canClassifyCustomRows
- ? rows.filter(
- (row) =>
- row.userId !== null &&
- row.status === "active" &&
- typeof row.serverUrl === "string" &&
- row.serverUrl.length > 0 &&
- !catalogSlugs.has(row.serverSlug),
- )
- : []
+ const slackInstallHref = `${BACKEND}/brain/slack/oauth/install`
+
+ const disconnectSlack = async () => {
+ try {
+ const res = await fetch(`${BACKEND}/brain/slack/workspace`, {
+ method: "DELETE",
+ credentials: "include",
+ })
+ if (res.status === 403) {
+ toast.error("Only admins can disconnect Slack.")
+ return
+ }
+ if (!res.ok) {
+ toast.error("Couldn't disconnect Slack.")
+ return
+ }
+ } catch {
+ toast.error("Couldn't disconnect Slack.")
+ return
+ }
+ toast.success("Slack disconnected.")
+ await load().catch(() => undefined)
+ }
+
+ const slackCard = (
+
+ )
+
+ const appCard = (entry: CatalogEntry) => (
+ connect(entry, shared)}
+ onDisconnect={(shared) => disconnect(entry, shared)}
+ />
+ )
+
return (
-
- {loading ? (
- <>
-
-
-
- >
- ) : (
- <>
-
{
- try {
- const res = await fetch(`${BACKEND}/brain/slack/workspace`, {
- method: "DELETE",
- credentials: "include",
- })
- if (res.status === 403) {
- toast.error("Only admins can disconnect Slack.")
- return
- }
- if (!res.ok) {
- toast.error("Couldn't disconnect Slack.")
- return
- }
- } catch {
- toast.error("Couldn't disconnect Slack.")
- return
- }
- toast.success("Slack disconnected.")
- await load().catch(() => undefined)
- }}
- />
- {apps.map((entry) => (
- connect(entry, shared)}
- onDisconnect={(shared) => disconnect(entry, shared)}
- />
- ))}
- {customRows.map((row) => (
- {}}
- onDisconnect={() =>
- disconnect(
- {
- slug: row.serverSlug,
- name: titleCase(row.serverSlug.replace(/-/g, " ")),
- category: "Custom OAuth MCP",
- authType: "oauth",
- },
- false,
- )
- }
- />
- ))}
- setCustomOpen(true)}
- className={cn(
- dmSans125ClassName(),
- "flex min-h-[104px] cursor-pointer items-center justify-center gap-2 rounded-xl border border-[#2A313C] border-dashed",
- "text-[13px] font-medium text-[#737B87] transition-colors hover:border-[#3A4150] hover:text-[#FAFAFA]",
- )}
- >
-
- Add custom MCP
-
- >
- )}
+
+ {loading ? (
+
+
+
+
+
+ ) : searching ? (
+
+ {slackMatches || catalogMatches.length > 0 ? (
+
+ {slackMatches ? slackCard : null}
+ {catalogMatches.map((entry) => (
+
{appCard(entry)}
+ ))}
+
+ ) : null}
+
0}
+ />
+
+ ) : (
+
+ {hasInstalled ? (
+
+ Installed
+
+ {slackConnected ? (
+
}
+ >
+ {isAdmin ? (
+ <>
+
+ Reconnect
+
+
{
+ if (window.confirm("Disconnect Slack?")) {
+ void disconnectSlack()
+ }
+ }}
+ >
+ Disconnect
+
+ >
+ ) : (
+
+ Managed by workspace admins
+
+ )}
+
+ ) : null}
+ {installedApps.map((entry) => {
+ const userConnected = isConnected(entry.slug, false)
+ const orgConnected = isConnected(entry.slug, true)
+ return (
+
+ {isAdmin ? (
+ <>
+
+ userConnected
+ ? disconnect(entry, false)
+ : connect(entry, false)
+ }
+ >
+ {userConnected
+ ? "Disconnect my account"
+ : "Connect my account"}
+
+
+ orgConnected
+ ? disconnect(entry, true)
+ : connect(entry, true)
+ }
+ >
+ {orgConnected
+ ? "Disconnect workspace"
+ : "Connect for workspace"}
+
+ >
+ ) : userConnected ? (
+ disconnect(entry, false)}
+ >
+ Disconnect
+
+ ) : (
+
+ Managed by workspace admins
+
+ )}
+
+ )
+ })}
+ {customRows.map((row) => (
+
+
+ disconnect(
+ {
+ slug: row.serverSlug,
+ name: customConnectionName(row.serverSlug),
+ category: "Custom MCP",
+ authType: "oauth",
+ },
+ false,
+ )
+ }
+ >
+ Disconnect
+
+
+ ))}
+
+
+ ) : null}
+
+ {!slackConnected ? (
+ {slackCard}
+ ) : null}
+ {recommendedApps.map((entry) => (
+
+ {appCard(entry)}
+
+ ))}
+ {recommendedDirectoryEntries.map((entry) => (
+
+
+
+ ))}
+
+
+
+
Marketplace
+ {marketplaceEntries.length > 0 ? (
+
+ {marketplaceEntries.length.toLocaleString()} servers
+
+ ) : null}
+
+ {marketplaceCategories.length > 0 ? (
+
+ {["all", ...marketplaceCategories].map((category) => (
+ setMarketplaceCategory(category)}
+ className={cn(
+ "h-7 shrink-0 cursor-pointer whitespace-nowrap rounded-full px-3 text-[12px] font-medium transition-colors",
+ marketplaceCategory === category
+ ? "bg-[#252B34] font-semibold text-[#FAFAFA]"
+ : "text-[#737373] hover:bg-[#14161A] hover:text-[#D4D4D8]",
+ )}
+ >
+ {category === "all" ? "All" : categoryLabel(category)}
+
+ ))}
+
+ ) : null}
+
+
+
+ )}
+
{/* Reset on every close path so the API key never lingers in state. */}
+ onOpenChange={(open: boolean) =>
open ? setCustomOpen(true) : resetCustomForm()
}
>
@@ -755,11 +1099,14 @@ export default function CompanyBrainConnections() {
- Add custom connector
+ {directoryEntry
+ ? `Set up ${directoryEntry.name}`
+ : "Add custom connector"}
- Connect your Brain to any remote MCP server. Signs in with OAuth
- unless you add an API key below.
+ {directoryEntry?.availability === "tenant"
+ ? "Enter your workspace-specific MCP URL, then choose how this server authenticates."
+ : "Confirm the remote MCP URL, then choose how this server authenticates."}
-
setCustomAdvancedOpen((open) => !open)}
- className="mt-1 flex items-center gap-1.5 self-start text-[13px] font-medium text-[#FAFAFA]"
+
-
- Advanced settings
-
+ {(["oauth", "api-key"] as const)
+ .filter(
+ (method) =>
+ !directoryEntry ||
+ directoryEntry.authMethods.includes(method),
+ )
+ .map((method) => (
+ setCustomAuthMethod(method)}
+ className={cn(
+ "h-8 rounded-full text-[12px] font-semibold transition-colors",
+ customAuthMethod === method
+ ? "bg-[#252B34] text-[#FAFAFA]"
+ : "text-[#737373] hover:text-[#D4D4D8]",
+ )}
+ >
+ {method === "oauth" ? "OAuth" : "API key"}
+
+ ))}
+
- {customAdvancedOpen && (
-
-
setCustomToken(event.target.value)}
- type="password"
- placeholder="API key (optional)"
- className={customInputClass}
+ {customAuthMethod === "api-key" && (
+
setCustomToken(event.target.value)}
+ type="password"
+ placeholder="API key"
+ required
+ className={customInputClass}
+ />
+ )}
+
+ {customAuthMethod === "api-key" && (
+
setCustomAdvancedOpen((open) => !open)}
+ className="mt-1 flex items-center gap-1.5 self-start text-[13px] font-medium text-[#FAFAFA]"
+ >
+
+ Header settings
+
+ )}
+
+ {customAuthMethod === "api-key" && customAdvancedOpen && (
+
setCustomHeaderName(event.target.value)}
diff --git a/apps/web/components/settings/mcp-directory-browser.tsx b/apps/web/components/settings/mcp-directory-browser.tsx
new file mode 100644
index 00000000..4fe323ee
--- /dev/null
+++ b/apps/web/components/settings/mcp-directory-browser.tsx
@@ -0,0 +1,329 @@
+"use client"
+
+import { Loader2 } from "lucide-react"
+import { useEffect, useMemo, useState } from "react"
+import type { McpDirectoryEntry } from "@/lib/mcp-directory"
+import { brainConnectorIcon } from "../brain-connector-icons"
+import { ConnectorCard, ScopeChip } from "../directory/connector-card"
+import { PillButton } from "../integrations/install-steps"
+
+const BACKEND =
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
+
+let directoryCache: McpDirectoryEntry[] | null = null
+
+function isDirectoryEntry(value: unknown): value is McpDirectoryEntry {
+ if (!value || typeof value !== "object") return false
+ const entry = value as Partial
+ return (
+ typeof entry.id === "string" &&
+ typeof entry.name === "string" &&
+ (entry.type === "remote" || entry.type === "local") &&
+ (entry.url === null || typeof entry.url === "string") &&
+ typeof entry.auth === "string" &&
+ (entry.note === null || typeof entry.note === "string") &&
+ Array.isArray(entry.categories) &&
+ entry.categories.every((category) => typeof category === "string") &&
+ typeof entry.popularity === "number" &&
+ (entry.iconDomain === null || typeof entry.iconDomain === "string") &&
+ ["custom", "unsupported"].includes(entry.setup ?? "") &&
+ (entry.oauthCapability === null ||
+ ["dcr", "preregistered"].includes(entry.oauthCapability ?? "")) &&
+ Array.isArray(entry.authMethods) &&
+ entry.authMethods.every((method) =>
+ ["oauth", "api-key"].includes(method),
+ ) &&
+ ["fixed", "tenant", "unavailable", "local"].includes(
+ entry.availability ?? "",
+ )
+ )
+}
+
+function parseDirectory(value: unknown) {
+ if (!value || typeof value !== "object") throw new Error("invalid catalog")
+ const entries = (value as { entries?: unknown }).entries
+ if (!Array.isArray(entries) || !entries.every(isDirectoryEntry)) {
+ throw new Error("invalid catalog")
+ }
+ return entries
+}
+
+async function loadDirectory(signal: AbortSignal) {
+ if (directoryCache) return directoryCache
+ const response = await fetch(`${BACKEND}/brain/mcp-connections/directory`, {
+ signal,
+ cache: "default",
+ credentials: "include",
+ })
+ if (!response.ok) throw new Error("catalog request failed")
+ directoryCache = parseDirectory(await response.json())
+ return directoryCache
+}
+
+export function useMcpDirectory() {
+ const [entries, setEntries] = useState(
+ () => directoryCache ?? [],
+ )
+ const [error, setError] = useState(false)
+
+ useEffect(() => {
+ const controller = new AbortController()
+ void loadDirectory(controller.signal)
+ .then((data) => {
+ setEntries(data)
+ setError(false)
+ })
+ .catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === "AbortError") return
+ setError(true)
+ })
+ return () => controller.abort()
+ }, [])
+
+ return { entries, error }
+}
+
+export function categoryLabel(value: string) {
+ return value
+ .split("-")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(" ")
+}
+
+export function entrySlug(entry: McpDirectoryEntry) {
+ return entry.name
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "")
+ .slice(0, 63)
+}
+
+// Mirrors the backend's URL normalization so connection rows match entries.
+export function normalizeServerUrl(value: string) {
+ try {
+ const url = new URL(value)
+ return `${url.protocol}//${url.host}${url.pathname.replace(/\/+$/, "")}`.toLowerCase()
+ } catch {
+ return value.toLowerCase()
+ }
+}
+
+// An entry we can actually take the user through connecting.
+export function isEntrySetUppable(entry: McpDirectoryEntry) {
+ return (
+ entry.setup !== "unsupported" &&
+ entry.authMethods.length > 0 &&
+ (entry.availability === "fixed" || entry.availability === "tenant")
+ )
+}
+
+// Entries worth listing at all — servers with no reachable URL are dropped.
+export function listableDirectoryEntries(entries: McpDirectoryEntry[]) {
+ return entries.filter((entry) => entry.availability !== "unavailable")
+}
+
+export function entryMatchesQuery(entry: McpDirectoryEntry, needle: string) {
+ return [entry.name, entry.url, entry.note, ...entry.categories]
+ .filter(Boolean)
+ .some((value) => value?.toLowerCase().includes(needle))
+}
+
+function DirectoryIcon({ entry }: { entry: McpDirectoryEntry }) {
+ const [failed, setFailed] = useState(false)
+ if (!entry.iconDomain || failed) {
+ return brainConnectorIcon(entrySlug(entry), entry.name, "size-4")
+ }
+ return (
+ setFailed(true)}
+ />
+ )
+}
+
+export function DirectoryEntryCard({
+ entry,
+ connected,
+ onSetUp,
+}: {
+ entry: McpDirectoryEntry
+ connected: boolean
+ onSetUp: (entry: McpDirectoryEntry) => void
+}) {
+ const canSetUp = !connected && isEntrySetUppable(entry)
+ const status = connected
+ ? "Connected"
+ : canSetUp
+ ? "Not connected"
+ : entry.availability === "local"
+ ? "Desktop only"
+ : "Coming soon"
+ return (
+ }
+ name={entry.name}
+ subtitle={entrySubtitle(entry)}
+ footerLeft={ }
+ footerRight={
+ canSetUp ? (
+ onSetUp(entry)}>Set up
+ ) : null
+ }
+ />
+ )
+}
+
+function entrySubtitle(entry: McpDirectoryEntry) {
+ if (entry.categories.length > 0) {
+ return entry.categories.slice(0, 2).map(categoryLabel).join(" · ")
+ }
+ return entry.type === "local" ? "Desktop extension" : "MCP server"
+}
+
+// One directory listing: a dense single-line row. The default state carries no
+// status text — in a marketplace, "not connected" is implied. Only connection,
+// or the reason there's no button, earns words.
+export function DirectoryEntryRow({
+ entry,
+ connected,
+ onSetUp,
+}: {
+ entry: McpDirectoryEntry
+ connected: boolean
+ onSetUp: (entry: McpDirectoryEntry) => void
+}) {
+ const canSetUp = !connected && isEntrySetUppable(entry)
+ return (
+
+
+
+
+
+
+ {entry.name}
+
+
+ {entrySubtitle(entry)}
+
+
+ {connected ? (
+
+
+ Connected
+
+ ) : canSetUp ? (
+
onSetUp(entry)}
+ className="h-7 shrink-0 cursor-pointer rounded-full bg-[#1B2028] px-3 text-[12px] font-medium text-[#FAFAFA]/70 transition-colors group-hover:bg-[#252C37] group-hover:text-[#FAFAFA] hover:bg-[#2B3340]"
+ >
+ Set up
+
+ ) : (
+
+ {entry.availability === "local" ? "Desktop only" : "Coming soon"}
+
+ )}
+
+ )
+}
+
+const GRID_PAGE_SIZE = 24
+
+// Paged card grid over the MCP directory. With a query it renders matching
+// servers; without one it renders the whole marketplace.
+export function McpDirectoryGrid({
+ query = "",
+ entries,
+ loadError,
+ excludeSlugs,
+ isEntryConnected,
+ onSetUp,
+ suppressEmpty,
+}: {
+ query?: string
+ entries: McpDirectoryEntry[]
+ loadError: boolean
+ // entries already rendered elsewhere (e.g. the built-in app catalog)
+ excludeSlugs?: Set
+ isEntryConnected: (entry: McpDirectoryEntry) => boolean
+ onSetUp: (entry: McpDirectoryEntry) => void
+ // the caller rendered its own matches, so an empty grid isn't "no results"
+ suppressEmpty?: boolean
+}) {
+ const [visibleCount, setVisibleCount] = useState(GRID_PAGE_SIZE)
+ const needle = query.trim().toLowerCase()
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: reset paging per query
+ useEffect(() => {
+ setVisibleCount(GRID_PAGE_SIZE)
+ }, [needle])
+
+ // Connected first, then connectable, then "coming soon"/desktop-only.
+ const matches = useMemo(() => {
+ const found = entries.filter(
+ (entry) =>
+ !excludeSlugs?.has(entrySlug(entry)) &&
+ (!needle || entryMatchesQuery(entry, needle)),
+ )
+ return found.sort(
+ (a, b) =>
+ Number(isEntryConnected(b)) - Number(isEntryConnected(a)) ||
+ Number(isEntrySetUppable(b)) - Number(isEntrySetUppable(a)),
+ )
+ }, [entries, excludeSlugs, isEntryConnected, needle])
+
+ if (loadError) {
+ if (suppressEmpty) return null
+ return (
+
+ The MCP directory couldn't be loaded. Refresh to try again.
+
+ )
+ }
+ if (entries.length === 0) {
+ if (suppressEmpty) return null
+ return (
+
+
+ Loading MCP directory
+
+ )
+ }
+ if (matches.length === 0) {
+ if (suppressEmpty) return null
+ return (
+
+ No integrations match “{query.trim()}”.
+
+ )
+ }
+ return (
+
+
+ {matches.slice(0, visibleCount).map((entry) => (
+
+ ))}
+
+ {visibleCount < matches.length ? (
+
setVisibleCount((count) => count + GRID_PAGE_SIZE)}
+ className="mx-auto flex h-9 cursor-pointer items-center rounded-full border border-[#2A313C] px-5 text-[12px] font-semibold text-[#D4D4D8] transition-colors hover:border-[#3A4150] hover:text-[#FAFAFA]"
+ >
+ Show {Math.min(GRID_PAGE_SIZE, matches.length - visibleCount)} more ·{" "}
+ {visibleCount} of {matches.length.toLocaleString()}
+
+ ) : null}
+
+ )
+}
diff --git a/apps/web/lib/mcp-directory.ts b/apps/web/lib/mcp-directory.ts
new file mode 100644
index 00000000..e1e3c3f5
--- /dev/null
+++ b/apps/web/lib/mcp-directory.ts
@@ -0,0 +1,21 @@
+export type McpDirectoryAvailability =
+ | "fixed"
+ | "tenant"
+ | "unavailable"
+ | "local"
+
+export type McpDirectoryEntry = {
+ id: string
+ name: string
+ type: "remote" | "local"
+ url: string | null
+ auth: string
+ note: string | null
+ categories: string[]
+ popularity: number
+ availability: McpDirectoryAvailability
+ iconDomain: string | null
+ setup: "custom" | "unsupported"
+ oauthCapability: "dcr" | "preregistered" | null
+ authMethods: Array<"oauth" | "api-key">
+}
diff --git a/apps/web/lib/mcp-icon-domains.json b/apps/web/lib/mcp-icon-domains.json
new file mode 100644
index 00000000..2989996d
--- /dev/null
+++ b/apps/web/lib/mcp-icon-domains.json
@@ -0,0 +1,518 @@
+{
+ "domains": [
+ "10xgenomics.com",
+ "activecampaign.com",
+ "actively.ai",
+ "adisinsight-mcp.springer.com",
+ "adobe-creativity.adobe.io",
+ "adobeaemcloud.com",
+ "aep-ai-ama.adobe.io",
+ "affinity.co",
+ "aftership.com",
+ "agent.thoughtspot.app",
+ "agentmail.to",
+ "agents.riskanalytics.dnb.com",
+ "agenttools.wolfram.com",
+ "ahrefs.com",
+ "ai-connect.norton.com",
+ "ai-inc.mailchimp.com",
+ "ai-inc.quickbooks.intuit.com",
+ "ai-inc.turbotax.intuit.com",
+ "ai-tools.tillermoney.com",
+ "ai.chronograph.pe",
+ "ai.consilio.com",
+ "ai.thirdbridge.com",
+ "ai.todoist.net",
+ "ai.veltra.com",
+ "airbnb.com",
+ "airtable.com",
+ "ajo-mcp.adobe.io",
+ "alltrails.com",
+ "alma.food",
+ "alphavantage.co",
+ "alphaxiv.org",
+ "alpic.ai",
+ "amplitude.com",
+ "analytics.credit.morningstar.com",
+ "analytics.lseg.com",
+ "android.com",
+ "angellist.com",
+ "anthropic.mcp.creditkarma.com",
+ "api-ssl.bitly.com",
+ "apify.com",
+ "apigw.americanexpress.com",
+ "apollo.io",
+ "apollographql.com",
+ "app.airops.com",
+ "app.base44.com",
+ "app.brighthire.ai",
+ "app.carta.com",
+ "app.definely.com",
+ "app.eraser.io",
+ "app.files.com",
+ "app.flourish.studio",
+ "app.fyxer.com",
+ "app.grasp-ai.com",
+ "app.hanoverpark.com",
+ "app.ketryx.com",
+ "app.magicschool.ai",
+ "app.midpage.ai",
+ "app.synthesize.bio",
+ "app.tropicapp.io",
+ "app.unthread.io",
+ "appfolio.com",
+ "asana.com",
+ "ashbyhq.com",
+ "asset-management.mcp.cloudinary.com",
+ "atlassian.com",
+ "attention.tech",
+ "attio.com",
+ "audible.com",
+ "auraintelligence.com",
+ "autodesk.com",
+ "autorfp.ai",
+ "benchling.com",
+ "benevity.org",
+ "bigdata.com",
+ "bigquery.googleapis.com",
+ "bindings.mcp.cloudflare.com",
+ "blockscout.com",
+ "blueconic.com",
+ "boltz.bio",
+ "box.com",
+ "brandfetch.io",
+ "brave.com",
+ "braze.com",
+ "brevo.com",
+ "brex.com",
+ "briskteaching.com",
+ "calendar.google.com",
+ "calendly.com",
+ "callbacks.omniapp.co",
+ "canary-data.com",
+ "candid.org",
+ "canva.com",
+ "cargoai.co",
+ "cbinsights.com",
+ "chargebee.com",
+ "chartmogul.com",
+ "chatgpt.mermaid.ai",
+ "checkatrade.com",
+ "circleback.ai",
+ "civitatis-claude-app.civitatis.com",
+ "cja-mcp.adobe.io",
+ "clapi.guidepoint.io",
+ "clarify.ai",
+ "clarity-sfdr20-mcp.pro.clarity.ai",
+ "claude-mcp-api.ml.goodnotes.com",
+ "claude.mcp.kpler.com",
+ "claude.slidesgpt.com",
+ "claudecompanion.gateway.api.mcafee.com",
+ "clay.com",
+ "clerk.com",
+ "clickhouse.cloud",
+ "clickup.com",
+ "close.com",
+ "cloud.cdata.com",
+ "cloudimanage.com",
+ "cloze.com",
+ "cognitoforms.com",
+ "coindesk.com",
+ "columnapi.com",
+ "cometchat.com",
+ "commonroom.io",
+ "compute.googleapis.com",
+ "connect.squareup.com",
+ "connector.scholargateway.ai",
+ "consensus.app",
+ "contentsquare.com",
+ "context.era.app",
+ "context7.com",
+ "coralogix.com",
+ "coteach.ai",
+ "coupler.io",
+ "coursera.com",
+ "courtlistener.com",
+ "courtroom5.com",
+ "craft.do",
+ "crossbeam.com",
+ "crypto.com",
+ "customer.io",
+ "daloopa.com",
+ "dashboard.plaid.com",
+ "data-search.apigw.feverup.com",
+ "databricks.com",
+ "datacamp.com",
+ "datadoghq.com",
+ "datagrail.io",
+ "datahub.com",
+ "day.ai",
+ "deepl.com",
+ "demandapi-mcp.booking.com",
+ "descript.com",
+ "descrybe.com",
+ "developer.api.autodesk.com",
+ "developer.mcp.mastercard.com",
+ "devrev.ai",
+ "dhsprogram.com",
+ "dice.com",
+ "diffit.me",
+ "digits.com",
+ "directbooker.ai",
+ "docs.superhuman.com",
+ "docuseal.com",
+ "docusign.com",
+ "dovetail.com",
+ "dremio.com",
+ "drive.google.com",
+ "dropbox.com",
+ "dynatrace.com",
+ "econ-index.mcp.claude.com",
+ "elevenlabs.io",
+ "elicit.com",
+ "entendre.finance",
+ "eulerapp.com",
+ "everlaw.com",
+ "exa.ai",
+ "example-server.modelcontextprotocol.io",
+ "excalidraw.com",
+ "exp-app-mcp.prod.ep.viator.com",
+ "expedia.com",
+ "expo.dev",
+ "factset.com",
+ "fathom.ai",
+ "fellow.app",
+ "felt.com",
+ "fids-mcp.ice.com",
+ "fig-mcp.instacart.com",
+ "figma.com",
+ "financeanalytics.dnb.com",
+ "financialmodelingprep.com",
+ "fireflies.ai",
+ "firefox.com",
+ "fiscal.ai",
+ "fitch.group",
+ "floot.com",
+ "frontify-integrations.com",
+ "fullstory.com",
+ "funnel.io",
+ "g.runorion.com",
+ "g2.com",
+ "gainsight.com",
+ "gamma.app",
+ "gatewaymcp.verisk.com",
+ "genai-prod-ext.dominos.co.in",
+ "getaugust.ai",
+ "getguru.com",
+ "getmontecarlo.com",
+ "getunblocked.com",
+ "glean.com",
+ "global.datasite.com",
+ "glovoapp.com",
+ "gmail.com",
+ "gocardless.com",
+ "godaddy.com",
+ "gopigment.com",
+ "govcon.dev",
+ "govtribe.com",
+ "grain.com",
+ "granola.ai",
+ "grantedai.com",
+ "grasshopper-mcp.prd.narmitech.com",
+ "grounding.kensho.com",
+ "gusto.com",
+ "harmonic.ai",
+ "harness.io",
+ "harvey.ai",
+ "haveibeenpwned.com",
+ "hcls.mcp.claude.com",
+ "healthex.io",
+ "helium10.com",
+ "heygen.com",
+ "highspot.com",
+ "honeycomb.io",
+ "hrn-production.helix.com",
+ "hubspot.com",
+ "huggingface.co",
+ "ibisworld.com",
+ "ibkr.com",
+ "idiolect.app",
+ "ifttt.com",
+ "imedidata.com",
+ "incident.io",
+ "indeed.com",
+ "inductive.bio",
+ "inkbox.ai",
+ "insiderone.com",
+ "instrumentl.com",
+ "intapp.com",
+ "integrators.prod.api.tabsplatform.com",
+ "intercom.com",
+ "ipone.clarivate.com",
+ "ironcladapp.com",
+ "isometric.com",
+ "item.app",
+ "jam.dev",
+ "jentic.com",
+ "jotform.com",
+ "jupiterone.com",
+ "jusmundi.com",
+ "k.owkin.com",
+ "kfinance.kensho.com",
+ "kg.mcp.learningcommons.org",
+ "kindora-mcp.azurewebsites.net",
+ "kiwi.com",
+ "klaviyo.com",
+ "krisp.ai",
+ "kubernetes.io",
+ "lastminute.com",
+ "latch.bio",
+ "latticehq.com",
+ "lawve.ai",
+ "learn.microsoft.com",
+ "leaveadot.com",
+ "legal-mcp.thomsonreuters.com",
+ "legaldatahunter.com",
+ "legalzoom.com",
+ "letsbot.net",
+ "letsdeel.com",
+ "light.inc",
+ "lightfield.app",
+ "lilt.com",
+ "linear.app",
+ "listenlabs.ai",
+ "litmus.com",
+ "livestorm.co",
+ "localfalcon.com",
+ "lorikeetcx.ai",
+ "lovable.dev",
+ "lucid.app",
+ "luminpdf.com",
+ "lumonic.com",
+ "lunarcrush.ai",
+ "lusha.com",
+ "macaly.com",
+ "magicpatterns.com",
+ "mail.superhuman.com",
+ "mailerlite.com",
+ "make.com",
+ "manufact.com",
+ "marketplace-mcp.us-east-1.api.aws",
+ "matrixmcp.virtuoso.ai",
+ "mcp-app.turkishtechlab.com",
+ "mcp-demo.airwallex.com",
+ "mcp-gateway-external-pilot.spotify.net",
+ "mcp-pub.aiera.com",
+ "mcp-public.basecamp-research.com",
+ "mcp-server.egnyte.com",
+ "mcp-server.signnow.com",
+ "mcp-server.zomato.com",
+ "mcp-v1.tixel.com",
+ "mcp2.readwise.io",
+ "meetcampfire.com",
+ "melon.com",
+ "meltwater.com",
+ "mem.ai",
+ "mem0.ai",
+ "mercadolibre.com",
+ "mercury.com",
+ "metabase.com",
+ "metal.ai",
+ "metaview.ai",
+ "microsoft.com",
+ "mintlify.com",
+ "miro.com",
+ "mixpanel.com",
+ "monday.com",
+ "mongodb.com",
+ "moodys.com",
+ "morningstar.com",
+ "mospi.gov.in",
+ "motherduck.com",
+ "msci.com",
+ "mtnewswires.com",
+ "myisolved.com",
+ "n8n.io",
+ "netlify-mcp.netlify.app",
+ "netsuite.com",
+ "nimbleway.com",
+ "nlp.api.production.unwrap.ai",
+ "nooks.in",
+ "notion.com",
+ "omni.mulesoft.com",
+ "onesignal.com",
+ "ontra.ai",
+ "open-ai-app.stubhub.net",
+ "oreilly.com",
+ "otter.ai",
+ "ottotheagent.com",
+ "outreach.io",
+ "pagerduty.com",
+ "pandadoc.com",
+ "partner-mcp.ticketmaster.com",
+ "patlytics.ai",
+ "paypal.com",
+ "paytmpayments.com",
+ "peec.ai",
+ "pga.com",
+ "phished.io",
+ "phoenix.hginsights.com",
+ "pi.security",
+ "pinegap.ai",
+ "platform.opentargets.org",
+ "plaud.ai",
+ "playmcp.kakao.com",
+ "polaranalytics.com",
+ "pophive.org",
+ "posthog.com",
+ "postman.com",
+ "premium.mcp.pitchbook.com",
+ "privacy.com",
+ "process.st",
+ "prod.originhq.com",
+ "production.ai-mcp-extensibility-prd.tamg.cloud",
+ "projects.motionapp.com",
+ "pscale.dev",
+ "public-api.wordpress.com",
+ "pubmed.mcp.claude.com",
+ "qbo-connector.meridian.pilot.com",
+ "qonto.com",
+ "quartr.com",
+ "quicknode.com",
+ "quo.com",
+ "railway.com",
+ "rallyuxr.com",
+ "ramp-mcp-remote.ramp.com",
+ "ramp.com",
+ "rapid7.com",
+ "razorpay.com",
+ "react.dev",
+ "read.ai",
+ "reclaim.ai",
+ "reddit.com",
+ "relativity.com",
+ "remote.com",
+ "render.com",
+ "replit-mcp.com",
+ "resend.com",
+ "retool.com",
+ "revolut.com",
+ "rillet.com",
+ "roamresearch.com",
+ "roboflow.com",
+ "salesflare.com",
+ "salesloft.com",
+ "sanity.io",
+ "sap.com",
+ "scamguard.malwarebytes.com",
+ "scite.ai",
+ "seismic.com",
+ "semrush.com",
+ "send.co",
+ "sentry.dev",
+ "servicenow.com",
+ "services.biorender.com",
+ "services.functionhealth.com",
+ "services.oxfordeconomics.com",
+ "setup.shopify.com",
+ "shapes.co",
+ "shipbob.com",
+ "shippo.com",
+ "shutterstock.com",
+ "sigmacomputing.com",
+ "signeasy.com",
+ "similarweb.com",
+ "sketch.com",
+ "sketchup.com",
+ "slack.com",
+ "smartbear.com",
+ "smartling.com",
+ "smartsheet.com",
+ "snowflake.com",
+ "snowstorm-mcp.snomedtools.org",
+ "snyk.io",
+ "solveintelligence.com",
+ "sourcegraph.com",
+ "spinach.ai",
+ "splice.com",
+ "sprouts-mcp-server.kartikay-dhar.workers.dev",
+ "squareup.com",
+ "stackoverflow.com",
+ "staircase.ai",
+ "starburst.io",
+ "strava.com",
+ "stripe.com",
+ "stytch.dev",
+ "sumble.com",
+ "sumsub.com",
+ "supabase.com",
+ "super.com",
+ "supermetrics.com",
+ "surveymonkey.com",
+ "swagger.mcp.smartbear.com",
+ "sybill.ai",
+ "synapse.org",
+ "tableau.com",
+ "taskrabbit.com",
+ "tavily.com",
+ "taxact.com",
+ "teacher-tools.eedi.ai",
+ "teamtailor.com",
+ "techgc.co",
+ "tellme.embat.io",
+ "thumbtack.com",
+ "tickettailor.ai",
+ "ticktick.com",
+ "tigerdata.com",
+ "tines.com",
+ "tldraw-mcp-app.tldraw.workers.dev",
+ "tldv.io",
+ "tomtom.com",
+ "tray.io",
+ "trellis.law",
+ "trello.com",
+ "trivago.com",
+ "tryprofound.com",
+ "turquoise.health",
+ "twilio.com",
+ "uakozrqrztgrgwoywxkx.supabase.co",
+ "uber.com",
+ "ubereats.com",
+ "udemy.com",
+ "unsplash.com",
+ "use.kick.co",
+ "usepylon.com",
+ "v0.app",
+ "vast.blueskyapi.com",
+ "vendr.com",
+ "vercel.com",
+ "vibe.com",
+ "virtuoso.ai",
+ "voluum.com",
+ "webexapis.com",
+ "webflow.com",
+ "webull.com",
+ "whimsical.com",
+ "windsor.ai",
+ "wisdom-api.enterpret.com",
+ "wisprflow.ai",
+ "within.ai",
+ "wix.com",
+ "workable.com",
+ "workato.com",
+ "workfront.adobe.com",
+ "workos.com",
+ "wrike.com",
+ "wyndhamhotels.com",
+ "xactrestore-xactremodelserver-usw2-prod.propsol.io",
+ "xero.com",
+ "xweather.com",
+ "zapier.com",
+ "ziprecruiter.com",
+ "zocks.io",
+ "zoho.com",
+ "zoom.us",
+ "zoominfo.com",
+ "zscaler.com"
+ ]
+}
From 183e9fba9375fb1d7681f4aaddd0c06a084f5edc Mon Sep 17 00:00:00 2001
From: ved015 <122012786+ved015@users.noreply.github.com>
Date: Fri, 21 Aug 2026 21:24:38 +0530
Subject: [PATCH 35/37] fix(openai-sdk-python): harden v4 migration
---
packages/openai-sdk-python/README.md | 7 ++--
packages/openai-sdk-python/pyproject.toml | 5 ++-
.../src/supermemory_openai/middleware.py | 16 ++++-----
.../src/supermemory_openai/tools.py | 33 ++++++++++---------
.../openai-sdk-python/tests/test_tools.py | 19 ++++++++---
packages/openai-sdk-python/uv.lock | 8 ++---
6 files changed, 52 insertions(+), 36 deletions(-)
diff --git a/packages/openai-sdk-python/README.md b/packages/openai-sdk-python/README.md
index f9d96510..b030a9fa 100644
--- a/packages/openai-sdk-python/README.md
+++ b/packages/openai-sdk-python/README.md
@@ -245,8 +245,7 @@ tools = SupermemoryTools(
# Search memories
result = await tools.search_memories(
information_to_get="user preferences",
- limit=10,
- include_full_docs=True
+ limit=10
)
# Add memory
@@ -260,6 +259,10 @@ result = await tools.fetch_memory(
)
```
+`include_full_docs` is retained as a deprecated Python argument for compatibility,
+but v4 search returns relevant memories and chunks instead of full source documents.
+It is no longer exposed in the OpenAI tool schema.
+
### Individual Tools
```python
diff --git a/packages/openai-sdk-python/pyproject.toml b/packages/openai-sdk-python/pyproject.toml
index 9e991881..210b2ae8 100644
--- a/packages/openai-sdk-python/pyproject.toml
+++ b/packages/openai-sdk-python/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "supermemory-openai-sdk"
-version = "1.0.5"
+version = "1.0.6"
description = "Memory tools for OpenAI function calling with supermemory"
readme = "README.md"
license = "MIT"
@@ -15,7 +15,6 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -62,7 +61,7 @@ multi_line_output = 3
line_length = 88
[tool.mypy]
-python_version = "3.8"
+python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
diff --git a/packages/openai-sdk-python/src/supermemory_openai/middleware.py b/packages/openai-sdk-python/src/supermemory_openai/middleware.py
index 1a079fa2..9cbad1b0 100644
--- a/packages/openai-sdk-python/src/supermemory_openai/middleware.py
+++ b/packages/openai-sdk-python/src/supermemory_openai/middleware.py
@@ -222,15 +222,15 @@ async def add_memory_tool(
) -> None:
"""Add a new memory to the SuperMemory system."""
try:
- add_params = {
- "content": content,
- "container_tag": container_tag,
- }
- if custom_id is not None:
- add_params["custom_id"] = custom_id
-
# Handle both sync and async supermemory clients
- result = client.add(**add_params)
+ if custom_id is None:
+ result = client.add(content=content, container_tag=container_tag)
+ else:
+ result = client.add(
+ content=content,
+ container_tag=container_tag,
+ custom_id=custom_id,
+ )
if inspect.isawaitable(result):
response = await result
else:
diff --git a/packages/openai-sdk-python/src/supermemory_openai/tools.py b/packages/openai-sdk-python/src/supermemory_openai/tools.py
index 60274615..d294bf58 100644
--- a/packages/openai-sdk-python/src/supermemory_openai/tools.py
+++ b/packages/openai-sdk-python/src/supermemory_openai/tools.py
@@ -1,7 +1,8 @@
"""Supermemory tools for OpenAI function calling."""
import json
-from typing import Dict, List, Optional, TypedDict, Union
+import warnings
+from typing import Dict, List, Optional, TypedDict
import supermemory
from openai.types.chat import (
@@ -10,7 +11,6 @@ from openai.types.chat import (
ChatCompletionToolMessageParam,
)
from supermemory.types import AddResponse, SearchMemoriesResponse
-from supermemory.types.search_memories_response import Result
from .exceptions import (
SupermemoryConfigurationError,
@@ -23,6 +23,8 @@ class SupermemoryToolsConfig(TypedDict, total=False):
"""Configuration for Supermemory tools.
Only one of `project_id` or `container_tags` can be provided.
+ The first container tag is the primary v4 search scope; all configured tags
+ are applied when adding a memory.
"""
base_url: Optional[str]
@@ -38,7 +40,7 @@ class MemorySearchResult(TypedDict, total=False):
"""Result type for memory search operations."""
success: bool
- results: Optional[List[Result]]
+ results: Optional[List[Dict[str, object]]]
count: Optional[int]
error: Optional[str]
@@ -65,14 +67,6 @@ MEMORY_TOOL_SCHEMAS: Dict[str, ChatCompletionFunctionToolParam] = {
"type": "string",
"description": "Terms to search for in the user's memories",
},
- "include_full_docs": {
- "type": "boolean",
- "description": (
- "Whether to include the full document content in the response. "
- "Defaults to true for better AI context."
- ),
- "default": True,
- },
"limit": {
"type": "number",
"description": "Maximum number of results to return",
@@ -169,23 +163,32 @@ class SupermemoryTools:
async def search_memories(
self,
information_to_get: str,
- include_full_docs: bool = True,
+ include_full_docs: Optional[bool] = None,
limit: int = 10,
) -> MemorySearchResult:
"""Search memories.
Args:
information_to_get: Terms to search for
- include_full_docs: Whether to include full document content
+ include_full_docs: Deprecated compatibility argument. V4 search
+ returns relevant memories and chunks, not full source documents.
limit: Maximum number of results
Returns:
MemorySearchResult
"""
+ if include_full_docs is not None:
+ warnings.warn(
+ "include_full_docs is deprecated and ignored because v4 search "
+ "does not return full source documents",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
try:
response: SearchMemoriesResponse = await self.client.search.memories(
q=information_to_get,
- container_tags=self.container_tags,
+ container_tag=self.container_tags[0],
limit=limit,
threshold=0.6,
search_mode="hybrid",
@@ -317,7 +320,7 @@ class SearchMemoriesTool:
async def execute(
self,
information_to_get: str,
- include_full_docs: bool = True,
+ include_full_docs: Optional[bool] = None,
limit: int = 10,
) -> MemorySearchResult:
"""Execute search memories."""
diff --git a/packages/openai-sdk-python/tests/test_tools.py b/packages/openai-sdk-python/tests/test_tools.py
index 5bc95920..029c4610 100644
--- a/packages/openai-sdk-python/tests/test_tools.py
+++ b/packages/openai-sdk-python/tests/test_tools.py
@@ -159,6 +159,10 @@ class TestToolDefinitions:
assert search_tool is not None
assert search_tool["type"] == "function"
assert "information_to_get" in search_tool["function"]["parameters"]["required"]
+ assert (
+ "include_full_docs"
+ not in search_tool["function"]["parameters"]["properties"]
+ )
# Check addMemory
add_tool = next(
@@ -206,25 +210,32 @@ class TestMemoryOperationsUnit:
@pytest.mark.asyncio
async def test_search_memories_uses_search_memories_hybrid(self):
- """search_memories must call client.search.memories with hybrid mode."""
+ """V4 search must use the primary singular tag and hybrid mode."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
- tools = SupermemoryTools("test-key", {"container_tags": ["unit-tag"]})
+ tools = SupermemoryTools(
+ "test-key", {"container_tags": ["primary-tag", "secondary-tag"]}
+ )
tools.client.search.memories = AsyncMock(
return_value=SimpleNamespace(
results=[SimpleNamespace(model_dump=lambda: {"memory": "likes tea"})]
)
)
- result = await tools.search_memories("tea", limit=3)
+ with pytest.warns(DeprecationWarning, match="include_full_docs"):
+ result = await tools.search_memories(
+ "tea", include_full_docs=False, limit=3
+ )
assert result["success"] is True
assert result["count"] == 1
tools.client.search.memories.assert_awaited_once()
kwargs = tools.client.search.memories.await_args.kwargs
assert kwargs["q"] == "tea"
- assert kwargs["container_tags"] == ["unit-tag"]
+ assert kwargs["container_tag"] == "primary-tag"
+ assert "container_tags" not in kwargs
+ assert "include_full_docs" not in kwargs
assert kwargs["limit"] == 3
assert kwargs["search_mode"] == "hybrid"
diff --git a/packages/openai-sdk-python/uv.lock b/packages/openai-sdk-python/uv.lock
index 6c457622..b4e297e5 100644
--- a/packages/openai-sdk-python/uv.lock
+++ b/packages/openai-sdk-python/uv.lock
@@ -377,7 +377,7 @@ resolution-markers = [
"python_full_version < '3.10'",
]
dependencies = [
- { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
wheels = [
@@ -392,7 +392,7 @@ resolution-markers = [
"python_full_version >= '3.10'",
]
dependencies = [
- { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
@@ -422,7 +422,7 @@ name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
@@ -1372,7 +1372,7 @@ wheels = [
[[package]]
name = "supermemory-openai-sdk"
-version = "1.0.4"
+version = "1.0.6"
source = { editable = "." }
dependencies = [
{ name = "openai" },
From d21d66138085382143c2f14c5d41ac17df8d8c73 Mon Sep 17 00:00:00 2001
From: ved015 <122012786+ved015@users.noreply.github.com>
Date: Sat, 22 Aug 2026 12:13:25 +0530
Subject: [PATCH 36/37] fix(tools): harden seven-tool parity
---
.github/workflows/ci.yml | 17 ++
bun.lock | 2 +-
packages/tools/package.json | 4 +-
packages/tools/src/ai-sdk.ts | 33 ++-
packages/tools/src/claude-memory.test.ts | 99 ++++---
packages/tools/src/claude-memory.ts | 321 ++++++++++++++++-----
packages/tools/src/openai/tools.ts | 34 ++-
packages/tools/src/tool-operations.test.ts | 51 +++-
packages/tools/src/tools-shared.ts | 180 +++++++++++-
packages/tools/src/voltagent/middleware.ts | 61 ++--
packages/tools/src/voltagent/types.ts | 5 +-
11 files changed, 614 insertions(+), 193 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 80600ae5..62670c9b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,5 +29,22 @@ jobs:
- name: Run TypeScript type checking
run: bunx turbo run check-types --filter='@supermemory/ai-sdk' --filter='@supermemory/memory-graph'
+ - name: Detect Tools package changes
+ id: tools-changes
+ run: |
+ if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/tools; then
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Run Tools unit tests
+ if: steps.tools-changes.outputs.changed == 'true'
+ run: bun run --cwd packages/tools test:unit
+
+ - name: Build Tools package
+ if: steps.tools-changes.outputs.changed == 'true'
+ run: bun run --cwd packages/tools build
+
- name: Run Biome CI (format & lint on changed files)
run: bunx biome ci --changed --since=origin/main --no-errors-on-unmatched
diff --git a/bun.lock b/bun.lock
index 3eb49d31..2084333c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -336,7 +336,7 @@
},
"packages/tools": {
"name": "@supermemory/tools",
- "version": "2.1.1",
+ "version": "2.2.0",
"dependencies": {
"@ai-sdk/anthropic": "^2.0.25",
"@ai-sdk/openai": "^2.0.23",
diff --git a/packages/tools/package.json b/packages/tools/package.json
index a4d0034f..74f2f5bc 100644
--- a/packages/tools/package.json
+++ b/packages/tools/package.json
@@ -1,14 +1,14 @@
{
"name": "@supermemory/tools",
"type": "module",
- "version": "2.1.1",
+ "version": "2.2.0",
"description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory",
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch --ignore-watch .turbo",
"check-types": "tsc --noEmit",
"test": "vitest --testTimeout 100000",
- "test:unit": "vitest run --testTimeout 100000 src/tools-shared.test.ts src/tool-operations.test.ts test/with-supermemory/unit.test.ts test/with-supermemory/conversation-conversion.test.ts test/mastra/unit.test.ts",
+ "test:unit": "vitest run --testTimeout 100000 src/tools-shared.test.ts src/tool-operations.test.ts src/claude-memory.test.ts test/with-supermemory/unit.test.ts test/with-supermemory/conversation-conversion.test.ts test/mastra/unit.test.ts",
"test:watch": "vitest --watch --testTimeout 100000"
},
"dependencies": {
diff --git a/packages/tools/src/ai-sdk.ts b/packages/tools/src/ai-sdk.ts
index b7e437db..1b710808 100644
--- a/packages/tools/src/ai-sdk.ts
+++ b/packages/tools/src/ai-sdk.ts
@@ -5,6 +5,7 @@ import {
DEFAULT_VALUES,
PARAMETER_DESCRIPTIONS,
TOOL_DESCRIPTIONS,
+ deleteDocumentByIdentifier,
getContainerTags,
} from "./tools-shared"
import { forgetMemoryRequest } from "./shared/forget-memory"
@@ -56,12 +57,12 @@ export const searchMemoriesTool = (
limit = DEFAULT_VALUES.limit,
}) => {
try {
- const response = await client.search({
+ const response = await client.search.documents({
q: informationToGet,
- ...(containerTags[0] ? { containerTag: containerTags[0] } : {}),
+ containerTags,
limit,
- threshold: DEFAULT_VALUES.chunkThreshold,
- searchMode: "hybrid",
+ chunkThreshold: DEFAULT_VALUES.chunkThreshold,
+ includeFullDocs,
})
return {
@@ -196,10 +197,12 @@ export const documentListTool = (
}),
execute: async ({ containerTag, limit, page }) => {
try {
- const tag = containerTag || containerTags[0]
+ const scopeTags: [string, ...string[]] = containerTag
+ ? [containerTag]
+ : containerTags
const response = await client.documents.list({
- containerTags: [tag],
+ containerTags: scopeTags,
limit: limit || DEFAULT_VALUES.limit,
...(page !== undefined && { page }),
})
@@ -227,15 +230,29 @@ export const documentDeleteTool = (
apiKey,
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
})
+ const containerTags = getContainerTags(config)
+ const strict = config?.strict ?? false
return tool({
description: TOOL_DESCRIPTIONS.documentDelete,
inputSchema: z.object({
documentId: z.string().describe(PARAMETER_DESCRIPTIONS.documentId),
+ containerTag: strict
+ ? z
+ .string()
+ .nullable()
+ .describe(PARAMETER_DESCRIPTIONS.documentContainerTag)
+ : z
+ .string()
+ .optional()
+ .describe(PARAMETER_DESCRIPTIONS.documentContainerTag),
}),
- execute: async ({ documentId }) => {
+ execute: async ({ documentId, containerTag }) => {
try {
- await client.documents.delete(documentId)
+ const scopeTags: [string, ...string[]] = containerTag
+ ? [containerTag]
+ : containerTags
+ await deleteDocumentByIdentifier(client, documentId, scopeTags)
return {
success: true,
diff --git a/packages/tools/src/claude-memory.test.ts b/packages/tools/src/claude-memory.test.ts
index 7d16493f..afe16dcf 100644
--- a/packages/tools/src/claude-memory.test.ts
+++ b/packages/tools/src/claude-memory.test.ts
@@ -1,18 +1,22 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
-// Mock the Supermemory SDK so the Claude memory tool's `view`/`readFile` path
-// can be exercised deterministically without any network access. We only need
-// `client.search()` to return a single document with known multi-line content.
-const searchMock = vi.fn()
+// Mock the Supermemory SDK so the Claude memory tool's document-backed file
+// operations can be exercised deterministically without any network access.
+const documentsListMock = vi.fn()
+const documentsGetMock = vi.fn()
+const documentsDeleteBulkMock = vi.fn()
const addMock = vi.fn()
vi.mock("supermemory", () => {
return {
default: class MockSupermemory {
- search = searchMock
add = addMock
memories = { forget: vi.fn() }
- documents = { delete: vi.fn() }
+ documents = {
+ list: documentsListMock,
+ get: documentsGetMock,
+ deleteBulk: documentsDeleteBulkMock,
+ }
},
}
})
@@ -22,20 +26,58 @@ import { ClaudeMemoryTool } from "./claude-memory"
const FILE_PATH = "/memories/notes.txt"
// 5 distinct lines so an off-by-one at either end is observable.
const FILE_CONTENT = "line1\nline2\nline3\nline4\nline5"
+const FILE_DOCUMENT = {
+ id: "document-notes",
+ customId: "memories_notes_txt",
+ filePath: FILE_PATH,
+ content: FILE_CONTENT,
+}
+const NEIGHBOUR_DOCUMENT = {
+ id: "document-notes-backup",
+ customId: "memories_notes_backup_txt",
+ filePath: "/memories/notes.backup.txt",
+ content: "backup stuff",
+}
+
+function mockDocuments(documents: typeof FILE_DOCUMENT[]) {
+ documentsListMock.mockResolvedValue({
+ memories: documents.map((document) => ({
+ id: document.id,
+ customId: document.customId,
+ containerTags: ["claude_memory"],
+ metadata: {
+ claude_memory_type: "file",
+ file_path: document.filePath,
+ },
+ })),
+ pagination: { totalPages: 1 },
+ })
+ documentsGetMock.mockImplementation(async (id: string) => {
+ const document = documents.find((candidate) => candidate.id === id)
+ if (!document) throw new Error(`Document not found: ${id}`)
+ return {
+ id: document.id,
+ customId: document.customId,
+ containerTags: ["sm_project_default", "claude_memory"],
+ metadata: {
+ claude_memory_type: "file",
+ file_path: document.filePath,
+ },
+ content: document.content,
+ }
+ })
+}
function mockDocument(content: string) {
- // `readFile` matches by `id === normalizePathToCustomId(path)`.
- // normalizePathToCustomId("/memories/notes.txt") -> "memories_notes_txt"
- searchMock.mockResolvedValue({
- results: [{ id: "memories_notes_txt", chunk: content }],
- })
+ mockDocuments([{ ...FILE_DOCUMENT, content }])
}
describe("ClaudeMemoryTool view_range", () => {
let tool: ClaudeMemoryTool
beforeEach(() => {
- searchMock.mockReset()
+ documentsListMock.mockReset()
+ documentsGetMock.mockReset()
mockDocument(FILE_CONTENT)
tool = new ClaudeMemoryTool("test-api-key")
})
@@ -90,18 +132,14 @@ describe("ClaudeMemoryTool exact-file matching", () => {
let tool: ClaudeMemoryTool
beforeEach(() => {
- searchMock.mockReset()
+ documentsListMock.mockReset()
+ documentsGetMock.mockReset()
addMock.mockReset()
tool = new ClaudeMemoryTool("test-api-key")
})
- it("view finds the exact file even when a neighbour ranks first", async () => {
- searchMock.mockResolvedValue({
- results: [
- { id: "memories_notes_backup_txt", chunk: "backup stuff" },
- { id: "memories_notes_txt", chunk: FILE_CONTENT },
- ],
- })
+ it("view finds the exact file even when a neighbour is listed first", async () => {
+ mockDocuments([NEIGHBOUR_DOCUMENT, FILE_DOCUMENT])
const result = await tool.handleCommand({
command: "view",
@@ -114,13 +152,9 @@ describe("ClaudeMemoryTool exact-file matching", () => {
})
it("view reports not-found instead of returning a different file", async () => {
- // Semantic search can surface a similarly-named file; that must not
+ // The document list can contain a similarly-named file; that must not
// be served as the requested one.
- searchMock.mockResolvedValue({
- results: [
- { id: "memories_notes_backup_txt", chunk: "backup stuff" },
- ],
- })
+ mockDocuments([NEIGHBOUR_DOCUMENT])
const result = await tool.handleCommand({
command: "view",
@@ -132,11 +166,7 @@ describe("ClaudeMemoryTool exact-file matching", () => {
})
it("str_replace refuses to modify a different file than requested", async () => {
- searchMock.mockResolvedValue({
- results: [
- { id: "memories_notes_backup_txt", chunk: "backup stuff" },
- ],
- })
+ mockDocuments([NEIGHBOUR_DOCUMENT])
const result = await tool.handleCommand({
command: "str_replace",
@@ -154,11 +184,10 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => {
let tool: ClaudeMemoryTool
beforeEach(() => {
- searchMock.mockReset()
+ documentsListMock.mockReset()
+ documentsGetMock.mockReset()
addMock.mockReset()
- searchMock.mockResolvedValue({
- results: [{ id: "memories_notes_txt", chunk: FILE_CONTENT }],
- })
+ mockDocument(FILE_CONTENT)
tool = new ClaudeMemoryTool("test-api-key")
})
diff --git a/packages/tools/src/claude-memory.ts b/packages/tools/src/claude-memory.ts
index ad05c108..3792e9f8 100644
--- a/packages/tools/src/claude-memory.ts
+++ b/packages/tools/src/claude-memory.ts
@@ -1,5 +1,5 @@
import Supermemory from "supermemory"
-import { getContainerTags } from "./tools-shared"
+import { deleteDocumentById, getContainerTags } from "./tools-shared"
import type { SupermemoryToolsConfig } from "./types"
// Claude Memory Tool Types
@@ -37,6 +37,14 @@ export interface MemoryToolResult {
is_error: boolean
}
+type ClaudeFileMetadata = Record
+
+interface ClaudeFileDocument {
+ documentId: string
+ content: string
+ metadata: ClaudeFileMetadata
+}
+
/**
* Claude Memory Tool - Client-side implementation
* Maps Claude's memory tool commands to supermemory document operations
@@ -44,6 +52,7 @@ export interface MemoryToolResult {
export class ClaudeMemoryTool {
private client: Supermemory
private containerTags: string[]
+ private scopeContainerTags: [string, ...string[]]
private memoryContainerPrefix: string
/**
@@ -68,6 +77,7 @@ export class ClaudeMemoryTool {
// Get base container tags and add memory-specific tag
const baseContainerTags = getContainerTags(config)
+ this.scopeContainerTags = baseContainerTags
this.containerTags = [...baseContainerTags, this.memoryContainerPrefix]
}
@@ -193,44 +203,89 @@ export class ClaudeMemoryTool {
*/
private async listDirectory(dirPath: string): Promise {
try {
- // Search for all memory files
- const response = await this.client.search({
- q: "*", // Search for all
- ...(this.containerTags[0]
- ? { containerTag: this.containerTags[0] }
- : {}),
- limit: 100, // Get many files (max allowed)
- searchMode: "hybrid",
- })
+ // Document search returns ranked chunks, not a complete inventory. Walk
+ // every page of the document-list endpoint so files cannot disappear
+ // from a directory merely because they did not rank in a search page.
+ const documents: Supermemory.DocumentListResponse.Memory[] = []
+ let page = 1
- if (!response.results) {
- return {
- success: true,
- content: `Directory: ${dirPath}\n(empty)`,
- }
+ while (true) {
+ const response = await this.client.documents.list({
+ containerTags: this.scopeContainerTags,
+ filters: {
+ AND: [
+ { key: "claude_memory_type", value: "file" },
+ {
+ key: "file_path",
+ value: dirPath,
+ filterType: "string_contains",
+ },
+ ],
+ },
+ includeContent: false,
+ limit: 100,
+ page,
+ })
+
+ documents.push(...response.memories)
+
+ if (page >= response.pagination.totalPages) break
+ page += 1
}
// Filter files that match the directory path and extract relative paths
const files: string[] = []
const dirs = new Set()
+ const candidates: Array<{
+ document: Supermemory.DocumentListResponse.Memory
+ filePath: string
+ }> = []
- for (const result of response.results) {
- // Get the file path from metadata (since customId is normalized)
- const filePath = result.metadata?.file_path as string
- if (!filePath || !filePath.startsWith(dirPath)) continue
+ for (const document of documents) {
+ if (!this.isDocumentInConfiguredScope(document)) continue
- // Get relative path from directory
- const relativePath = filePath.substring(dirPath.length)
- if (!relativePath) continue
+ const filePath = this.getDocumentFilePath(document)
+ if (!filePath || !filePath.startsWith(dirPath)) {
+ continue
+ }
+ candidates.push({ document, filePath })
+ }
- // If path contains /, it's in a subdirectory
- const slashIndex = relativePath.indexOf("/")
- if (slashIndex > 0) {
- // It's a subdirectory
- dirs.add(`${relativePath.substring(0, slashIndex)}/`)
- } else if (relativePath !== "") {
- // It's a file in this directory
- files.push(relativePath)
+ // Full GETs are required to verify hidden project tags. Keep them bounded
+ // so large directories do not become a long serial chain or a burst of
+ // unbounded requests.
+ const verificationBatchSize = 8
+ for (
+ let index = 0;
+ index < candidates.length;
+ index += verificationBatchSize
+ ) {
+ const batch = candidates.slice(index, index + verificationBatchSize)
+ const verified = await Promise.all(
+ batch.map(async (candidate) =>
+ (await this.isDirectoryDocumentInExactScope(candidate.document))
+ ? candidate
+ : undefined,
+ ),
+ )
+
+ for (const candidate of verified) {
+ if (!candidate) continue
+ const { filePath } = candidate
+
+ // Get relative path from directory
+ const relativePath = filePath.substring(dirPath.length)
+ if (!relativePath) continue
+
+ // If path contains /, it's in a subdirectory
+ const slashIndex = relativePath.indexOf("/")
+ if (slashIndex > 0) {
+ // It's a subdirectory
+ dirs.add(`${relativePath.substring(0, slashIndex)}/`)
+ } else if (relativePath !== "") {
+ // It's a file in this directory
+ files.push(relativePath)
+ }
}
}
@@ -264,10 +319,8 @@ export class ClaudeMemoryTool {
viewRange?: [number, number],
): Promise {
try {
- // Same lookup as every mutating command: limit 5 so the exact
- // customId match is findable among semantic near-neighbours.
- // With the old limit of 1, a similarly-named file ranking first
- // made this return the wrong file's contents as a success.
+ // Resolve the exact document inside the configured scope so reads and
+ // mutations use the complete stored file, not one ranked search chunk.
const readResult = await this.getFileDocument(filePath)
if (!readResult.success || !readResult.document) {
return {
@@ -278,7 +331,7 @@ export class ClaudeMemoryTool {
const document = readResult.document
- let content: string = document.raw || document.content || ""
+ let content = document.content
// Apply line range if specified
if (viewRange) {
@@ -376,8 +429,7 @@ export class ClaudeMemoryTool {
}
}
- const originalContent =
- readResult.document.raw || readResult.document.content || ""
+ const originalContent = readResult.document.content
// Check if old_str exists in the content
if (!originalContent.includes(oldStr)) {
@@ -435,8 +487,7 @@ export class ClaudeMemoryTool {
}
}
- const originalContent =
- readResult.document.raw || readResult.document.content || ""
+ const originalContent = readResult.document.content
const lines = originalContent.split("\n")
// Validate line number
@@ -490,9 +541,7 @@ export class ClaudeMemoryTool {
}
}
- const documentId =
- readResult.document.documentId ?? this.normalizePathToCustomId(filePath)
- await this.client.documents.delete(documentId)
+ await deleteDocumentById(this.client, readResult.document.documentId)
return {
success: true,
@@ -531,8 +580,7 @@ export class ClaudeMemoryTool {
}
}
- const originalContent =
- readResult.document.raw || readResult.document.content || ""
+ const originalContent = readResult.document.content
const newNormalizedId = this.normalizePathToCustomId(newPath)
// Create new document with new path
@@ -552,8 +600,7 @@ export class ClaudeMemoryTool {
// customId — the add above already replaced the content.
const oldNormalizedId = this.normalizePathToCustomId(oldPath)
if (oldNormalizedId !== newNormalizedId) {
- const oldDocumentId = readResult.document.documentId ?? oldNormalizedId
- await this.client.documents.delete(oldDocumentId)
+ await deleteDocumentById(this.client, readResult.document.documentId)
}
return {
@@ -573,48 +620,124 @@ export class ClaudeMemoryTool {
*/
private async getFileDocument(filePath: string): Promise<{
success: boolean
- document?: any
+ document?: ClaudeFileDocument
error?: string
}> {
try {
const normalizedId = this.normalizePathToCustomId(filePath)
+ let page = 1
+ const candidates = new Map<
+ string,
+ Supermemory.DocumentListResponse.Memory
+ >()
- const response = await this.client.search({
- q: normalizedId,
- ...(this.containerTags[0]
- ? { containerTag: this.containerTags[0] }
- : {}),
- limit: 5,
- searchMode: "hybrid",
- })
+ // customId values are only unique within an exact container-tag set in
+ // Mono. Resolve the matching document inside this tool's configured
+ // scope before fetching by internal ID; a direct get(customId) can pick
+ // another project/user's same-named file.
+ while (true) {
+ const response = await this.client.documents.list({
+ containerTags: this.scopeContainerTags,
+ filters: {
+ AND: [
+ { key: "claude_memory_type", value: "file" },
+ { key: "file_path", value: filePath },
+ ],
+ },
+ includeContent: false,
+ limit: 100,
+ page,
+ })
- // Only accept the exact customId match. Falling back to the top
- // semantic hit would let callers read — and worse, modify or
- // delete — a different file than the one they asked for.
- const match = response.results?.find(
- (r) =>
- r.id === normalizedId ||
- r.documents?.some((d) => d.id === normalizedId),
- )
+ for (const document of response.memories) {
+ if (
+ document.customId === normalizedId &&
+ this.getDocumentFilePath(document) === filePath &&
+ this.isDocumentInConfiguredScope(document)
+ ) {
+ candidates.set(document.id, document)
+ }
+ }
- if (!match) {
+ if (page >= response.pagination.totalPages) break
+ page += 1
+ }
+
+ const exactMatches: Array<{
+ candidate: Supermemory.DocumentListResponse.Memory
+ document: Supermemory.DocumentGetResponse
+ }> = []
+ let hasUnverifiedCandidate = false
+ for (const candidate of candidates.values()) {
+ let document: Supermemory.DocumentGetResponse
+ try {
+ document = await this.client.documents.get(candidate.id)
+ } catch (error) {
+ if (error instanceof Supermemory.NotFoundError) continue
+ throw error
+ }
+
+ if (document.id !== candidate.id) {
+ hasUnverifiedCandidate = true
+ continue
+ }
+ if (
+ document.customId !== normalizedId ||
+ this.getDocumentFilePath(document) !== filePath ||
+ !this.hasExactContainerTags(document.containerTags)
+ ) {
+ continue
+ }
+
+ exactMatches.push({ candidate, document })
+ }
+
+ if (exactMatches.length === 0) {
return {
success: false,
error: `File not found: ${filePath}`,
}
}
+ if (exactMatches.length > 1) {
+ return {
+ success: false,
+ error: `File path is ambiguous in the configured container scope: ${filePath}`,
+ }
+ }
+ if (hasUnverifiedCandidate) {
+ return {
+ success: false,
+ error: `File path could not be resolved unambiguously in the configured container scope: ${filePath}`,
+ }
+ }
- const content = match.chunk || match.memory || ""
- const documentId = match.documents?.[0]?.id ?? match.id
+ const match = exactMatches[0]
+ if (!match) {
+ return { success: false, error: `File not found: ${filePath}` }
+ }
+ const { candidate, document } = match
+ const content =
+ typeof document.content === "string"
+ ? document.content
+ : typeof document.raw === "string"
+ ? document.raw
+ : undefined
+ if (content === undefined) {
+ return {
+ success: false,
+ error: `File content unavailable: ${filePath}`,
+ }
+ }
+ const metadata =
+ document.metadata &&
+ typeof document.metadata === "object" &&
+ !Array.isArray(document.metadata)
+ ? (document.metadata as ClaudeFileMetadata)
+ : {}
return {
success: true,
- document: {
- documentId,
- content,
- raw: content,
- metadata: match.metadata,
- },
+ document: { documentId: candidate.id, content, metadata },
}
} catch (error) {
return {
@@ -624,6 +747,60 @@ export class ClaudeMemoryTool {
}
}
+ private getDocumentFilePath(document: {
+ metadata: unknown
+ }): string | undefined {
+ const metadata = document.metadata
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
+ return undefined
+ }
+ const metadataRecord = metadata as Record
+
+ return typeof metadataRecord.file_path === "string"
+ ? metadataRecord.file_path
+ : undefined
+ }
+
+ private isDocumentInConfiguredScope(
+ document: Supermemory.DocumentListResponse.Memory,
+ ): boolean {
+ const documentTags = document.containerTags ?? []
+ const expectedTags = this.containerTags.filter(
+ (tag) => !tag.startsWith("sm_project_"),
+ )
+
+ return (
+ documentTags.length === expectedTags.length &&
+ documentTags.every((tag, index) => tag === expectedTags[index])
+ )
+ }
+
+ private async isDirectoryDocumentInExactScope(
+ document: Supermemory.DocumentListResponse.Memory,
+ ): Promise {
+ try {
+ // Mono strips internal project tags from every list response, so only a
+ // full get can prove that no hidden tags change this document's scope.
+ const fullDocument = await this.client.documents.get(document.id)
+ return (
+ fullDocument.id === document.id &&
+ this.hasExactContainerTags(fullDocument.containerTags)
+ )
+ } catch (error) {
+ if (!(error instanceof Supermemory.NotFoundError)) throw error
+ // A document can disappear between list and get. Skip stale entries
+ // instead of failing the entire directory view.
+ return false
+ }
+ }
+
+ private hasExactContainerTags(containerTags?: string[]): boolean {
+ return (
+ containerTags?.length === this.containerTags.length &&
+ containerTags.every((tag, index) => tag === this.containerTags[index])
+ )
+ }
+
/**
* Validate that path starts with /memories for security
*/
diff --git a/packages/tools/src/openai/tools.ts b/packages/tools/src/openai/tools.ts
index 49b5c88a..06a26935 100644
--- a/packages/tools/src/openai/tools.ts
+++ b/packages/tools/src/openai/tools.ts
@@ -4,6 +4,7 @@ import {
DEFAULT_VALUES,
PARAMETER_DESCRIPTIONS,
TOOL_DESCRIPTIONS,
+ deleteDocumentByIdentifier,
getContainerTags,
} from "../tools-shared"
import { forgetMemoryRequest } from "../shared/forget-memory"
@@ -14,7 +15,9 @@ import type { SupermemoryToolsConfig } from "../types"
*/
export interface MemorySearchResult {
success: boolean
- results?: Awaited>["results"]
+ results?: Awaited<
+ ReturnType
+ >["results"]
count?: number
error?: string
}
@@ -31,7 +34,7 @@ export interface ProfileResult {
static: string[]
dynamic: string[]
}
- searchResults?: Awaited>
+ searchResults?: Awaited>["searchResults"]
error?: string
}
@@ -159,6 +162,10 @@ export const memoryToolSchemas = {
type: "string",
description: PARAMETER_DESCRIPTIONS.documentId,
},
+ containerTag: {
+ type: "string",
+ description: PARAMETER_DESCRIPTIONS.documentContainerTag,
+ },
},
required: ["documentId"],
},
@@ -248,12 +255,12 @@ export function createSearchMemoriesFunction(
limit?: number
}): Promise {
try {
- const response = await client.search({
+ const response = await client.search.documents({
q: informationToGet,
- ...(containerTags[0] ? { containerTag: containerTags[0] } : {}),
+ containerTags,
limit,
- threshold: DEFAULT_VALUES.chunkThreshold,
- searchMode: "hybrid",
+ chunkThreshold: DEFAULT_VALUES.chunkThreshold,
+ includeFullDocs,
})
return {
@@ -363,10 +370,12 @@ export function createDocumentListFunction(
page?: number
}): Promise {
try {
- const tag = containerTag || containerTags[0]
+ const scopeTags: [string, ...string[]] = containerTag
+ ? [containerTag]
+ : containerTags
const response = await client.documents.list({
- containerTags: [tag],
+ containerTags: scopeTags,
limit: limit || DEFAULT_VALUES.limit,
...(page !== undefined && { page }),
})
@@ -392,15 +401,20 @@ export function createDocumentDeleteFunction(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
- const { client } = createClient(apiKey, config)
+ const { client, containerTags } = createClient(apiKey, config)
return async function documentDelete({
documentId,
+ containerTag,
}: {
documentId: string
+ containerTag?: string
}): Promise {
try {
- await client.documents.delete(documentId)
+ const scopeTags: [string, ...string[]] = containerTag
+ ? [containerTag]
+ : containerTags
+ await deleteDocumentByIdentifier(client, documentId, scopeTags)
return {
success: true,
diff --git a/packages/tools/src/tool-operations.test.ts b/packages/tools/src/tool-operations.test.ts
index 8ec46fa0..c6152021 100644
--- a/packages/tools/src/tool-operations.test.ts
+++ b/packages/tools/src/tool-operations.test.ts
@@ -2,18 +2,18 @@ import { beforeEach, describe, expect, it, vi } from "vitest"
// Mock the Supermemory SDK (same pattern as claude-memory.test.ts) so tool
// executions can be verified deterministically without network access.
-const documentsDelete = vi.fn()
+const documentsDeleteBulk = vi.fn()
+const documentsGet = vi.fn()
const documentsList = vi.fn()
-const searchMock = vi.fn()
const clientAdd = vi.fn()
vi.mock("supermemory", () => {
return {
default: class MockSupermemory {
- search = searchMock
add = clientAdd
documents = {
- delete: documentsDelete,
+ deleteBulk: documentsDeleteBulk,
+ get: documentsGet,
list: documentsList,
add: vi.fn(),
}
@@ -35,12 +35,20 @@ function executeTool(tool: unknown, args: Record) {
}
beforeEach(() => {
- documentsDelete.mockReset().mockResolvedValue(undefined)
+ documentsDeleteBulk.mockReset().mockResolvedValue({
+ success: true,
+ deletedCount: 1,
+ errors: [],
+ })
+ documentsGet.mockReset().mockResolvedValue({
+ id: "doc_123",
+ customId: "doc_123",
+ containerTags: ["sm_project_default"],
+ })
documentsList.mockReset().mockResolvedValue({
memories: [{ id: "doc_1", title: "Doc one" }],
pagination: { currentPage: 1, totalItems: 1, totalPages: 1 },
})
- searchMock.mockReset()
clientAdd.mockReset().mockResolvedValue({ id: "doc_new" })
vi.unstubAllGlobals()
})
@@ -53,7 +61,8 @@ describe("documentDelete", () => {
}
expect(result.success).toBe(true)
- expect(documentsDelete).toHaveBeenCalledWith("doc_123")
+ expect(documentsGet).toHaveBeenCalledWith("doc_123")
+ expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: ["doc_123"] })
})
})
@@ -177,16 +186,30 @@ describe("memoryForget", () => {
describe("ClaudeMemoryTool", () => {
const FILE_PATH = "/memories/prefs.txt"
const CUSTOM_ID = "memories_prefs_txt"
+ const DOCUMENT_ID = "doc_file_1"
function mockFileDocument(content: string) {
- searchMock.mockResolvedValue({
- results: [
+ const metadata = {
+ claude_memory_type: "file",
+ file_path: FILE_PATH,
+ }
+ documentsList.mockResolvedValue({
+ memories: [
{
- id: CUSTOM_ID,
- chunk: content,
- metadata: { file_path: FILE_PATH },
+ id: DOCUMENT_ID,
+ customId: CUSTOM_ID,
+ containerTags: ["claude_memory"],
+ metadata,
},
],
+ pagination: { currentPage: 1, totalItems: 1, totalPages: 1 },
+ })
+ documentsGet.mockResolvedValue({
+ id: DOCUMENT_ID,
+ customId: CUSTOM_ID,
+ containerTags: ["sm_project_default", "claude_memory"],
+ content,
+ metadata,
})
}
@@ -247,7 +270,7 @@ describe("ClaudeMemoryTool", () => {
})
expect(result.success).toBe(true)
- expect(documentsDelete).toHaveBeenCalledWith(CUSTOM_ID)
+ expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: [DOCUMENT_ID] })
})
it("rename removes the old document after creating the new one", async () => {
@@ -264,6 +287,6 @@ describe("ClaudeMemoryTool", () => {
expect(clientAdd).toHaveBeenCalledWith(
expect.objectContaining({ customId: "memories_renamed_txt" }),
)
- expect(documentsDelete).toHaveBeenCalledWith(CUSTOM_ID)
+ expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: [DOCUMENT_ID] })
})
})
diff --git a/packages/tools/src/tools-shared.ts b/packages/tools/src/tools-shared.ts
index cbb1c1a7..dffc7093 100644
--- a/packages/tools/src/tools-shared.ts
+++ b/packages/tools/src/tools-shared.ts
@@ -2,48 +2,51 @@
* Shared constants and descriptions for Supermemory tools
*/
+import type Supermemory from "supermemory"
import type { MemoryMode } from "./shared/types"
// Tool descriptions
export const TOOL_DESCRIPTIONS = {
searchMemories:
- "Search (recall) stored memories for facts, preferences, history, and context about the user or any topic. Use proactively before answering whenever memory could help — do not wait for the user to explicitly ask you to search or recall. Search when the question touches personal context, past conversations, preferences, projects, people, plans, or anything you may have learned before. Results include memory/chunk IDs — use those IDs with memoryForget to remove a specific learned fact.",
+ "Search stored source documents for relevant facts, preferences, history, and other context. Use when explicitly asked to search or recall, or when past context could materially improve the response; do not invoke reflexively on every turn. Results contain document IDs and matching text chunks, not profile-memory IDs for memoryForget.",
addMemory:
"Add (remember) memories/details/information about the user or other facts or entities. Run when explicitly asked or when the user mentions any information generalizable beyond the context of the current conversation.",
getProfile:
- "Get user profile containing static memories (permanent facts) and dynamic memories (recent context). Optionally include search results by providing a query. Profile and search result entries may include memory IDs useful for memoryForget.",
+ "Get user profile containing static memories (permanent facts) and dynamic memories (recent context). Profile entries are text without IDs. Provide a query to include searchResults, whose memory entries may include IDs usable with memoryForget.",
documentList:
- "List stored source documents (conversations, URLs, files, pasted text) with pagination. Returns document IDs for documentDelete — not memory IDs for memoryForget. Use to browse raw stored content before permanently removing a source.",
+ "List stored source documents (conversations, URLs, files, pasted text) with pagination. Configured container tags are treated as the default union; an optional containerTag replaces that union with one tag for this operation. Returns document metadata and IDs for documentDelete, not raw document content or memory IDs for memoryForget.",
documentDelete:
- "Permanently delete a stored document and ALL memories extracted from it (hard delete). Use document IDs from documentList. Use when the user wants to remove an entire conversation, file, URL, or other source — not when correcting a single learned fact (use memoryForget for that).",
+ "Permanently delete a stored source document. Memories extracted from that source are soft-forgotten so they no longer appear in profile or search; they are not hard-deleted. Use a document ID or customId when removing an entire conversation, file, URL, or other source. The effective scope is the configured container-tag union, or the explicit one-tag override; if documentList used an override, pass the same value here. To forget one learned fact, use memoryForget instead.",
documentAdd:
"Store a source document for asynchronous processing and automatic memory extraction. Use when the user gives you raw content to ingest — a pasted text blob, conversation transcript, chat history, notes, URL, article link, or other substantial text — rather than a single atomic fact (use addMemory for one short generalizable sentence). The document is queued immediately; Supermemory post-processes it in the background (chunking, embedding, indexing) and extracts profile memories automatically — you do not need to call addMemory for facts buried inside the document. Good for saving full conversations, long-form notes, knowledge-base articles, meeting transcripts, or any large body of text the user wants remembered beyond this chat turn. Processing may take a moment; extracted memories appear in profile/search after indexing completes.",
memoryForget:
- "Soft-delete a single extracted profile memory (a learned fact) so it no longer appears in profile or search. Does NOT delete source documents. Provide memoryId (preferred — from searchMemories or getProfile) OR memoryContent for an exact text match. Use when the user retracts or corrects a specific fact (e.g. 'forget I like tea', 'that's wrong'). To remove an entire conversation or file, use documentDelete instead.",
+ "Soft-forget a single extracted profile memory (a learned fact) so it no longer appears in profile or search. Does NOT delete source documents. Provide memoryId from query-backed getProfile searchResults, or memoryContent for an exact text match; document and chunk IDs from searchMemories are not valid. Use when the user retracts or corrects a specific fact. To remove an entire source, use documentDelete instead.",
} as const
// Parameter descriptions
export const PARAMETER_DESCRIPTIONS = {
informationToGet:
- "What to look up in memory — keywords from the user's message, topic, entity names, or question phrasing. Search even when the user did not explicitly ask you to recall.",
+ "What to look up in stored context — keywords from the user's message, topic, entity names, or question phrasing.",
includeFullDocs:
"Whether to include the full document content in the response. Defaults to true for better AI context.",
limit: "Maximum number of results to return",
memory:
"The text content of the memory to add. This should be a single sentence or a short paragraph.",
containerTag: "Tag to filter/scope the operation (e.g., user ID, project ID)",
+ documentContainerTag:
+ "Optional one-tag scope override. When deleting a document returned by documentList with a containerTag override, pass the same value here. In strict mode, pass null to use the configured union.",
query: "Optional search query to include relevant search results",
page: "Page number to fetch, 1-based (default: 1)",
documentId:
- "Document ID from documentList — permanently deletes the source document and all extracted memories. Not a profile memory ID.",
+ "Document ID from documentList, or the document customId. Permanently deletes the source document and soft-forgets its extracted memories. If documentList used a containerTag override, pass it again. Not a profile-memory ID.",
content:
"Document body to store — plain text, a conversation transcript, a long pasted blob, or a URL to a webpage/PDF/image/video. Content is queued and memories are extracted automatically after background processing; do not split into addMemory calls.",
title: "Optional title for the document",
description: "Optional description for the document",
memoryId:
- "Profile memory ID from searchMemories or getProfile — soft-deletes one learned fact via memoryForget. Not a document ID.",
+ "Profile-memory ID from query-backed getProfile searchResults. Soft-forgets one learned fact; document and chunk IDs from searchMemories are not valid.",
memoryContent:
- "Exact text of the profile memory to forget (alternative to memoryId). Must match precisely; if unsure, search first and use memoryId.",
+ "Exact text of the profile memory to forget (alternative to memoryId). Must match precisely; if unsure, query getProfile and use a search-result memory ID.",
reason: "Optional reason recorded when forgetting (e.g. outdated, user correction)",
} as const
@@ -57,7 +60,7 @@ export const DEFAULT_VALUES = {
// Container tag constants
export const CONTAINER_TAG_CONSTANTS = {
projectPrefix: "sm_project_",
- defaultTags: ["sm_project_default"] as string[],
+ defaultTags: ["sm_project_default"] as const,
} as const
/**
@@ -66,16 +69,167 @@ export const CONTAINER_TAG_CONSTANTS = {
export function getContainerTags(config?: {
projectId?: string
containerTags?: string[]
-}): string[] {
+}): [string, ...string[]] {
if (config?.projectId !== undefined && config.containerTags !== undefined) {
throw new Error(
"Supermemory tools config accepts either projectId or containerTags, not both.",
)
}
- if (config?.projectId) {
+ if (config?.projectId !== undefined) {
+ if (config.projectId.trim() === "") {
+ throw new Error("Supermemory tools config requires a non-empty projectId.")
+ }
return [`${CONTAINER_TAG_CONSTANTS.projectPrefix}${config.projectId}`]
}
- return config?.containerTags ?? CONTAINER_TAG_CONSTANTS.defaultTags
+ if (config?.containerTags !== undefined) {
+ const [firstTag, ...remainingTags] = config.containerTags
+ if (
+ firstTag === undefined ||
+ config.containerTags.some((tag) => tag.trim() === "")
+ ) {
+ throw new Error(
+ "Supermemory tools config requires at least one non-empty containerTag.",
+ )
+ }
+ return [firstTag, ...remainingTags]
+ }
+ return [...CONTAINER_TAG_CONSTANTS.defaultTags]
+}
+
+/** Delete exactly one document by its internal ID. */
+export async function deleteDocumentById(
+ client: Supermemory,
+ documentId: string,
+): Promise {
+ const response = await client.documents.deleteBulk({ ids: [documentId] })
+ if (response.success && response.deletedCount === 1) return
+
+ const detail = response.errors?.find((error) => error.id === documentId)?.error
+ throw new Error(
+ detail
+ ? `Failed to delete document ${documentId}: ${detail}`
+ : `Failed to delete document ${documentId}: expected one deletion, received ${response.deletedCount}`,
+ )
+}
+
+/**
+ * Resolve an internal ID or customId inside the effective container-tag union,
+ * then delete the exact internal document ID. Internal IDs take precedence over
+ * customId matches.
+ */
+export async function deleteDocumentByIdentifier(
+ client: Supermemory,
+ documentIdentifier: string,
+ containerTags: readonly [string, ...string[]],
+): Promise {
+ const directMatch = await getDocumentIfFound(client, documentIdentifier)
+ if (
+ directMatch?.id === documentIdentifier &&
+ hasContainerTagOverlap(directMatch.containerTags, containerTags)
+ ) {
+ await deleteDocumentById(client, directMatch.id)
+ return
+ }
+
+ const candidateIds = new Set()
+ let hasInternalIdCandidate = false
+ let page = 1
+ while (true) {
+ const response = await client.documents.list({
+ containerTags: [...containerTags],
+ includeContent: false,
+ limit: 100,
+ page,
+ })
+ for (const document of response.memories) {
+ if (document.id === documentIdentifier) {
+ hasInternalIdCandidate = true
+ }
+ if (
+ document.id === documentIdentifier ||
+ document.customId === documentIdentifier
+ ) {
+ candidateIds.add(document.id)
+ }
+ }
+ if (page >= response.pagination.totalPages) break
+ page += 1
+ }
+
+ let exactIdMatch: string | undefined
+ let hasUnverifiedCandidate = false
+ const customIdMatches: string[] = []
+ for (const candidateId of candidateIds) {
+ const document = await getDocumentIfFound(client, candidateId)
+ if (document?.id !== candidateId) {
+ hasUnverifiedCandidate = true
+ continue
+ }
+ if (!hasContainerTagOverlap(document.containerTags, containerTags)) {
+ continue
+ }
+ if (document.id === documentIdentifier) {
+ exactIdMatch = document.id
+ break
+ }
+ if (document.customId === documentIdentifier) {
+ customIdMatches.push(document.id)
+ } else {
+ hasUnverifiedCandidate = true
+ }
+ }
+
+ if (exactIdMatch) {
+ await deleteDocumentById(client, exactIdMatch)
+ return
+ }
+ if (hasInternalIdCandidate) {
+ throw new Error(
+ `Document ID ${documentIdentifier} could not be verified safely in the configured container scope.`,
+ )
+ }
+ if (hasUnverifiedCandidate) {
+ throw new Error(
+ `Document identifier ${documentIdentifier} could not be resolved unambiguously in the configured container scope.`,
+ )
+ }
+ if (customIdMatches.length === 1) {
+ await deleteDocumentById(client, customIdMatches[0] as string)
+ return
+ }
+ if (customIdMatches.length > 1) {
+ throw new Error(
+ `Document customId ${documentIdentifier} is ambiguous in the configured container scope.`,
+ )
+ }
+ throw new Error(
+ `Document ${documentIdentifier} was not found in the configured container scope.`,
+ )
+}
+
+async function getDocumentIfFound(client: Supermemory, documentId: string) {
+ try {
+ return await client.documents.get(documentId)
+ } catch (error) {
+ if (isNotFoundError(error)) return undefined
+ throw error
+ }
+}
+
+function isNotFoundError(error: unknown): boolean {
+ return (
+ typeof error === "object" &&
+ error !== null &&
+ "status" in error &&
+ error.status === 404
+ )
+}
+
+function hasContainerTagOverlap(
+ actual: string[] | undefined,
+ expected: readonly string[],
+): boolean {
+ return actual?.some((tag) => expected.includes(tag)) ?? false
}
/**
diff --git a/packages/tools/src/voltagent/middleware.ts b/packages/tools/src/voltagent/middleware.ts
index bf771726..7e788c3d 100644
--- a/packages/tools/src/voltagent/middleware.ts
+++ b/packages/tools/src/voltagent/middleware.ts
@@ -18,7 +18,11 @@ import {
type Logger,
type MemoryMode,
} from "../shared"
-import type { SupermemoryVoltAgent, VoltAgentMessage } from "./types"
+import type {
+ SearchFilters,
+ SupermemoryVoltAgent,
+ VoltAgentMessage,
+} from "./types"
/**
* Context for Supermemory middleware operations.
@@ -47,7 +51,7 @@ export interface SupermemoryMiddlewareContext {
limit?: number
rerank?: boolean
rewriteQuery?: boolean
- filters?: { OR: Array } | { AND: Array }
+ filters?: SearchFilters
include?: {
chunks?: boolean
documents?: boolean
@@ -258,23 +262,7 @@ export const enhanceMessagesWithMemories = async (
if (useAdvancedSearch && ctx.mode !== "profile") {
ctx.logger.info("Using advanced search with custom parameters")
- const searchParams: {
- q: string
- containerTag: string
- threshold?: number
- limit?: number
- rerank?: boolean
- rewriteQuery?: boolean
- filters?: { OR: Array } | { AND: Array }
- include?: {
- chunks?: boolean
- documents?: boolean
- forgottenMemories?: boolean
- relatedMemories?: boolean
- summaries?: boolean
- }
- searchMode?: "memories" | "documents" | "hybrid"
- } = {
+ const searchParams: Supermemory.SearchParams = {
q: queryText,
containerTag: ctx.containerTag,
}
@@ -288,31 +276,32 @@ export const enhanceMessagesWithMemories = async (
if (ctx.include !== undefined) searchParams.include = ctx.include
if (ctx.searchMode !== undefined) searchParams.searchMode = ctx.searchMode
- const response = await ctx.client.search.memories(searchParams)
+ const response = await ctx.client.search(searchParams)
// Hybrid search returns both memory entries (`memory` field) and
- // document chunks (`chunk` field). Handle both.
- type SearchResult = {
- memory?: string
- chunk?: string
- metadata?: Record
- }
- const formattedMemories = response.results
- .map((result: SearchResult) => {
- const text = result.memory || result.chunk
- return text ? `- ${text}` : null
- })
- .filter(Boolean)
+ // document chunks (`chunk` field). Normalize both for prompt templates.
+ const searchResults = response.results.flatMap((result) => {
+ const memory = result.memory ?? result.chunk
+ if (!memory) {
+ return []
+ }
+
+ return [
+ {
+ memory,
+ ...(result.metadata ? { metadata: result.metadata } : {}),
+ },
+ ]
+ })
+ const formattedMemories = searchResults
+ .map((result) => `- ${result.memory}`)
.join("\n")
memories = ctx.promptTemplate
? ctx.promptTemplate({
userMemories: "",
generalSearchMemories: formattedMemories,
- searchResults: response.results as Array<{
- memory: string
- metadata?: Record
- }>,
+ searchResults,
})
: `The following are relevant memories and context about this user retrieved from previous interactions. Use these to personalize your response:\n\n${formattedMemories}`
} else {
diff --git a/packages/tools/src/voltagent/types.ts b/packages/tools/src/voltagent/types.ts
index cc5350ea..e6524ebc 100644
--- a/packages/tools/src/voltagent/types.ts
+++ b/packages/tools/src/voltagent/types.ts
@@ -5,6 +5,7 @@
* Supermemory by providing hooks that inject memories before LLM calls.
*/
+import type Supermemory from "supermemory"
import type {
PromptTemplate,
MemoryMode,
@@ -58,7 +59,7 @@ export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
/**
* Advanced filters to apply to the search using AND/OR logic.
- * Example: { OR: [{ metadata: { type: "note" } }, { metadata: { type: "conversation" } }] }
+ * Example: { OR: [{ key: "type", value: "note" }, { key: "type", value: "conversation" }] }
*
* Note: Only effective when mode is "query" or "full". Ignored in "profile" mode.
*/
@@ -99,7 +100,7 @@ export interface SupermemoryVoltAgent extends SupermemoryBaseOptions {
/**
* Advanced search filters using AND/OR logic
*/
-export type SearchFilters = { OR: Array } | { AND: Array }
+export type SearchFilters = NonNullable
/**
* Options for including additional data in search results
From 7153801f11dfdb50aeed6c4e0840ce79ccc7f805 Mon Sep 17 00:00:00 2001
From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com>
Date: Sat, 22 Aug 2026 06:46:13 +0000
Subject: [PATCH 37/37] fix(tools): resolve lint and format errors
- Replace `as any` with typed assertion in claude-memory.ts
- Apply Biome formatting fixes
Co-Authored-By: Claude Opus 4.5
---
packages/tools/src/claude-memory.test.ts | 34 +++++++++++++-----------
packages/tools/src/claude-memory.ts | 2 +-
packages/tools/src/openai/tools.ts | 4 +--
packages/tools/src/tools-shared.ts | 11 +++++---
4 files changed, 28 insertions(+), 23 deletions(-)
diff --git a/packages/tools/src/claude-memory.test.ts b/packages/tools/src/claude-memory.test.ts
index afe16dcf..7337441a 100644
--- a/packages/tools/src/claude-memory.test.ts
+++ b/packages/tools/src/claude-memory.test.ts
@@ -39,7 +39,7 @@ const NEIGHBOUR_DOCUMENT = {
content: "backup stuff",
}
-function mockDocuments(documents: typeof FILE_DOCUMENT[]) {
+function mockDocuments(documents: (typeof FILE_DOCUMENT)[]) {
documentsListMock.mockResolvedValue({
memories: documents.map((document) => ({
id: document.id,
@@ -191,20 +191,22 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => {
tool = new ClaudeMemoryTool("test-api-key")
})
- it.each(["$&", "$'", "$`", "$$"])(
- "stores %s literally instead of expanding it as a replacement pattern",
- async (dollarSequence) => {
- const result = await tool.handleCommand({
- command: "str_replace",
- path: FILE_PATH,
- old_str: "line3",
- new_str: `price is ${dollarSequence} today`,
- })
+ it.each([
+ "$&",
+ "$'",
+ "$`",
+ "$$",
+ ])("stores %s literally instead of expanding it as a replacement pattern", async (dollarSequence) => {
+ const result = await tool.handleCommand({
+ command: "str_replace",
+ path: FILE_PATH,
+ old_str: "line3",
+ new_str: `price is ${dollarSequence} today`,
+ })
- expect(result.success).toBe(true)
- expect(addMock).toHaveBeenCalledTimes(1)
- const stored = addMock.mock.calls[0]?.[0]?.content as string
- expect(stored).toContain(`price is ${dollarSequence} today`)
- },
- )
+ expect(result.success).toBe(true)
+ expect(addMock).toHaveBeenCalledTimes(1)
+ const stored = addMock.mock.calls[0]?.[0]?.content as string
+ expect(stored).toContain(`price is ${dollarSequence} today`)
+ })
})
diff --git a/packages/tools/src/claude-memory.ts b/packages/tools/src/claude-memory.ts
index 3792e9f8..867c4d82 100644
--- a/packages/tools/src/claude-memory.ts
+++ b/packages/tools/src/claude-memory.ts
@@ -150,7 +150,7 @@ export class ClaudeMemoryTool {
default:
return {
success: false,
- error: `Unknown command: ${(command as any).command}`,
+ error: `Unknown command: ${(command as { command: string }).command}`,
}
}
} catch (error) {
diff --git a/packages/tools/src/openai/tools.ts b/packages/tools/src/openai/tools.ts
index 06a26935..22257727 100644
--- a/packages/tools/src/openai/tools.ts
+++ b/packages/tools/src/openai/tools.ts
@@ -15,9 +15,7 @@ import type { SupermemoryToolsConfig } from "../types"
*/
export interface MemorySearchResult {
success: boolean
- results?: Awaited<
- ReturnType
- >["results"]
+ results?: Awaited>["results"]
count?: number
error?: string
}
diff --git a/packages/tools/src/tools-shared.ts b/packages/tools/src/tools-shared.ts
index dffc7093..c0a3b540 100644
--- a/packages/tools/src/tools-shared.ts
+++ b/packages/tools/src/tools-shared.ts
@@ -47,7 +47,8 @@ export const PARAMETER_DESCRIPTIONS = {
"Profile-memory ID from query-backed getProfile searchResults. Soft-forgets one learned fact; document and chunk IDs from searchMemories are not valid.",
memoryContent:
"Exact text of the profile memory to forget (alternative to memoryId). Must match precisely; if unsure, query getProfile and use a search-result memory ID.",
- reason: "Optional reason recorded when forgetting (e.g. outdated, user correction)",
+ reason:
+ "Optional reason recorded when forgetting (e.g. outdated, user correction)",
} as const
// Default values
@@ -77,7 +78,9 @@ export function getContainerTags(config?: {
}
if (config?.projectId !== undefined) {
if (config.projectId.trim() === "") {
- throw new Error("Supermemory tools config requires a non-empty projectId.")
+ throw new Error(
+ "Supermemory tools config requires a non-empty projectId.",
+ )
}
return [`${CONTAINER_TAG_CONSTANTS.projectPrefix}${config.projectId}`]
}
@@ -104,7 +107,9 @@ export async function deleteDocumentById(
const response = await client.documents.deleteBulk({ ids: [documentId] })
if (response.success && response.deletedCount === 1) return
- const detail = response.errors?.find((error) => error.id === documentId)?.error
+ const detail = response.errors?.find(
+ (error) => error.id === documentId,
+ )?.error
throw new Error(
detail
? `Failed to delete document ${documentId}: ${detail}`