Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/tools/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@supermemory/tools",
"type": "module",
"version": "2.3.0",
"version": "2.3.1",
"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 src/claude-memory.test.ts test/with-supermemory/unit.test.ts test/with-supermemory/conversation-conversion.test.ts test/openai-middleware.unit.test.ts test/mastra/unit.test.ts test/voltagent.unit.test.ts",
"test:unit": "vitest run --testTimeout 100000 src/tools-shared.test.ts src/tool-operations.test.ts src/document-delete-scope.test.ts src/claude-memory.test.ts test/with-supermemory/unit.test.ts test/with-supermemory/conversation-conversion.test.ts test/openai-middleware.unit.test.ts test/mastra/unit.test.ts test/voltagent.unit.test.ts",
"test:watch": "vitest --watch --testTimeout 100000"
},
"dependencies": {
Expand Down
11 changes: 6 additions & 5 deletions packages/tools/src/ai-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
clampSearchLimit,
deleteDocumentByIdentifier,
getContainerTags,
resolveConfiguredContainerTag,
} from "./tools-shared"
import { forgetMemoryRequest } from "./shared/forget-memory"
import type { SupermemoryToolsConfig } from "./types"
Expand Down Expand Up @@ -146,7 +147,7 @@ export const getProfileTool = (
}),
execute: async ({ containerTag, query }) => {
try {
const tag = containerTag || containerTags[0]
const tag = resolveConfiguredContainerTag(containerTags, containerTag)

const response = await client.profile({
containerTag: tag,
Expand Down Expand Up @@ -199,7 +200,7 @@ export const documentListTool = (
execute: async ({ containerTag, limit, page }) => {
try {
const scopeTags: [string, ...string[]] = containerTag
? [containerTag]
? [resolveConfiguredContainerTag(containerTags, containerTag)]
: containerTags

const response = await client.documents.list({
Expand Down Expand Up @@ -248,7 +249,7 @@ export const documentDeleteTool = (
execute: async ({ documentId, containerTag }) => {
try {
const scopeTags: [string, ...string[]] = containerTag
? [containerTag]
? [resolveConfiguredContainerTag(containerTags, containerTag)]
: containerTags
await deleteDocumentByIdentifier(client, documentId, scopeTags)

Expand Down Expand Up @@ -339,12 +340,12 @@ export const memoryForgetTool = (
}
}

const tag = containerTag || containerTags[0]
const tag = resolveConfiguredContainerTag(containerTags, containerTag)

await forgetMemoryRequest(
apiKey,
{
containerTag: tag as string,
containerTag: tag,
...(memoryId && { id: memoryId }),
...(memoryContent && { content: memoryContent }),
...(reason && { reason }),
Expand Down
139 changes: 139 additions & 0 deletions packages/tools/src/document-delete-scope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { beforeEach, describe, expect, it, vi } from "vitest"

const documentsDeleteBulk = vi.fn()
const documentsGet = vi.fn()
const documentsList = vi.fn()

vi.mock("supermemory", () => {
return {
default: class MockSupermemory {
documents = {
deleteBulk: documentsDeleteBulk,
get: documentsGet,
list: documentsList,
}
},
}
})

import * as aiSdk from "./ai-sdk"
import * as openAi from "./openai/tools"

const API_KEY = "sm_test_key"

type ToolExecutionResult = { success: boolean; error?: string }
type ToolWithExecute = {
execute: (args: Record<string, unknown>) => Promise<ToolExecutionResult>
}

function executeTool(
tool: unknown,
args: Record<string, unknown>,
): Promise<ToolExecutionResult> {
return (tool as ToolWithExecute).execute(args)
}

beforeEach(() => {
documentsDeleteBulk.mockReset().mockResolvedValue({
success: true,
deletedCount: 1,
errors: [],
})
documentsGet.mockReset()
documentsList.mockReset().mockResolvedValue({
memories: [],
pagination: { currentPage: 1, totalItems: 0, totalPages: 0 },
})
})

describe("documentDelete configured container scope", () => {
it("ai-sdk refuses a model-supplied tag outside the configured scope", async () => {
documentsGet.mockResolvedValue({
id: "doc_in_tenant_b",
customId: "doc_in_tenant_b",
containerTags: ["tenant-b"],
})

const result = await executeTool(
aiSdk.documentDeleteTool(API_KEY, { containerTags: ["tenant-a"] }),
{ documentId: "doc_in_tenant_b", containerTag: "tenant-b" },
)

expect(result.success).toBe(false)
expect(result.error).toContain("outside the configured scope")
expect(documentsGet).not.toHaveBeenCalled()
expect(documentsList).not.toHaveBeenCalled()
expect(documentsDeleteBulk).not.toHaveBeenCalled()
})

it("openai refuses a model-supplied tag outside the configured scope", async () => {
documentsGet.mockResolvedValue({
id: "doc_in_tenant_b",
customId: "doc_in_tenant_b",
containerTags: ["tenant-b"],
})

const documentDelete = openAi.createDocumentDeleteFunction(API_KEY, {
containerTags: ["tenant-a"],
})
const result = await documentDelete({
documentId: "doc_in_tenant_b",
containerTag: "tenant-b",
})

expect(result.success).toBe(false)
expect(result.error).toContain("outside the configured scope")
expect(documentsGet).not.toHaveBeenCalled()
expect(documentsList).not.toHaveBeenCalled()
expect(documentsDeleteBulk).not.toHaveBeenCalled()
})

it("rejects tags outside an implicit project scope", async () => {
const result = await executeTool(
aiSdk.documentDeleteTool(API_KEY, { projectId: "alpha" }),
{ documentId: "doc_1", containerTag: "tenant-b" },
)

expect(result.success).toBe(false)
expect(result.error).toContain("outside the configured scope")
expect(documentsDeleteBulk).not.toHaveBeenCalled()
})

it("allows selecting another explicitly configured tag", async () => {
documentsGet.mockResolvedValue({
id: "doc_in_tenant_b",
customId: "doc_in_tenant_b",
containerTags: ["tenant-b"],
})

const result = await executeTool(
aiSdk.documentDeleteTool(API_KEY, {
containerTags: ["tenant-a", "tenant-b"],
}),
{ documentId: "doc_in_tenant_b", containerTag: "tenant-b" },
)

expect(result.success).toBe(true)
expect(documentsDeleteBulk).toHaveBeenCalledWith({
ids: ["doc_in_tenant_b"],
})
})

it("keeps the configured union when no tag is supplied", async () => {
documentsGet.mockResolvedValue({
id: "doc_in_tenant_a",
customId: "doc_in_tenant_a",
containerTags: ["tenant-a"],
})

const result = await executeTool(
aiSdk.documentDeleteTool(API_KEY, { containerTags: ["tenant-a"] }),
{ documentId: "doc_in_tenant_a" },
)

expect(result.success).toBe(true)
expect(documentsDeleteBulk).toHaveBeenCalledWith({
ids: ["doc_in_tenant_a"],
})
})
})
11 changes: 6 additions & 5 deletions packages/tools/src/openai/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
clampSearchLimit,
deleteDocumentByIdentifier,
getContainerTags,
resolveConfiguredContainerTag,
} from "../tools-shared"
import { forgetMemoryRequest } from "../shared/forget-memory"
import type { SupermemoryToolsConfig } from "../types"
Expand Down Expand Up @@ -333,7 +334,7 @@ export function createGetProfileFunction(
query?: string
}): Promise<ProfileResult> {
try {
const tag = containerTag || containerTags[0]
const tag = resolveConfiguredContainerTag(containerTags, containerTag)

const response = await client.profile({
containerTag: tag,
Expand Down Expand Up @@ -374,7 +375,7 @@ export function createDocumentListFunction(
}): Promise<DocumentListResult> {
try {
const scopeTags: [string, ...string[]] = containerTag
? [containerTag]
? [resolveConfiguredContainerTag(containerTags, containerTag)]
: containerTags

const response = await client.documents.list({
Expand Down Expand Up @@ -415,7 +416,7 @@ export function createDocumentDeleteFunction(
}): Promise<DocumentDeleteResult> {
try {
const scopeTags: [string, ...string[]] = containerTag
? [containerTag]
? [resolveConfiguredContainerTag(containerTags, containerTag)]
: containerTags
await deleteDocumentByIdentifier(client, documentId, scopeTags)

Expand Down Expand Up @@ -502,12 +503,12 @@ export function createMemoryForgetFunction(
}
}

const tag = containerTag || containerTags[0]
const tag = resolveConfiguredContainerTag(containerTags, containerTag)

await forgetMemoryRequest(
apiKey,
{
containerTag: tag as string,
containerTag: tag,
...(memoryId && { id: memoryId }),
...(memoryContent && { content: memoryContent }),
...(reason && { reason }),
Expand Down
Loading
Loading