diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62670c9b..5262b786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,25 +26,43 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - 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 + - name: Detect SDK package changes + id: sdk-changes run: | if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/tools; then - echo "changed=false" >> "$GITHUB_OUTPUT" + echo "tools=false" >> "$GITHUB_OUTPUT" else - echo "changed=true" >> "$GITHUB_OUTPUT" + echo "tools=true" >> "$GITHUB_OUTPUT" + fi + + if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/ai-sdk; then + echo "ai_sdk=false" >> "$GITHUB_OUTPUT" + else + echo "ai_sdk=true" >> "$GITHUB_OUTPUT" fi - name: Run Tools unit tests - if: steps.tools-changes.outputs.changed == 'true' + if: steps.sdk-changes.outputs.tools == 'true' run: bun run --cwd packages/tools test:unit - name: Build Tools package - if: steps.tools-changes.outputs.changed == 'true' + if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' run: bun run --cwd packages/tools build + - name: Run AI SDK type checking + if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' + run: bun run --cwd packages/ai-sdk check-types + + - name: Run AI SDK unit tests + if: steps.sdk-changes.outputs.ai_sdk == 'true' + run: bun run --cwd packages/ai-sdk test:unit + + - name: Build AI SDK package + if: steps.sdk-changes.outputs.ai_sdk == 'true' + run: bun run --cwd packages/ai-sdk build + + - name: Run Memory Graph type checking + run: bun run --cwd packages/memory-graph check-types + - name: Run Biome CI (format & lint on changed files) run: bunx biome ci --changed --since=origin/main --no-errors-on-unmatched diff --git a/.github/workflows/publish-ai-sdk.yml b/.github/workflows/publish-ai-sdk.yml index 1ac60cf1..11559c0d 100644 --- a/.github/workflows/publish-ai-sdk.yml +++ b/.github/workflows/publish-ai-sdk.yml @@ -4,7 +4,7 @@ on: push: branches: - main - paths: + paths: - "packages/ai-sdk/package.json" concurrency: @@ -15,7 +15,7 @@ jobs: publish: runs-on: ubuntu-latest timeout-minutes: 15 - permissions: + permissions: contents: read id-token: write defaults: @@ -38,26 +38,65 @@ jobs: uses: oven-sh/setup-bun@v2 - name: Install dependencies - run: bun install + working-directory: . + run: bun install --frozen-lockfile - name: Check if version changed id: version-check run: | PACKAGE_NAME=$(jq -r '.name' package.json) LOCAL_VERSION=$(jq -r '.version' package.json) - NPM_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "0.0.0") - if [ "$LOCAL_VERSION" = "$NPM_VERSION" ]; then + if npm view "$PACKAGE_NAME@$LOCAL_VERSION" version >/dev/null 2>&1; then echo "Version $LOCAL_VERSION already published, skipping." echo "changed=false" >> "$GITHUB_OUTPUT" else - echo "Publishing $LOCAL_VERSION (npm has $NPM_VERSION)" + echo "Publishing $LOCAL_VERSION." echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Build + - name: Wait for the Tools dependency + if: steps.version-check.outputs.changed == 'true' + run: | + TOOLS_SPEC=$(jq -r '.dependencies["@supermemory/tools"]' package.json) + TOOLS_VERSION=${TOOLS_SPEC#^} + TOOLS_VERSION=${TOOLS_VERSION#~} + + if [[ ! "$TOOLS_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then + echo "Unsupported @supermemory/tools dependency spec: $TOOLS_SPEC" >&2 + exit 1 + fi + + for attempt in {1..20}; do + PUBLISHED_VERSION=$(npm view "@supermemory/tools@$TOOLS_VERSION" version 2>/dev/null || true) + if [ "$PUBLISHED_VERSION" = "$TOOLS_VERSION" ]; then + echo "@supermemory/tools@$TOOLS_VERSION is available on npm." + exit 0 + fi + + echo "Waiting for @supermemory/tools@$TOOLS_VERSION (attempt $attempt/20)." + sleep 15 + done + + echo "@supermemory/tools@$TOOLS_VERSION was not published within five minutes." >&2 + exit 1 + + - name: Build Tools dependency + if: steps.version-check.outputs.changed == 'true' + run: bun run --cwd ../tools build + + - name: Build AI SDK package if: steps.version-check.outputs.changed == 'true' run: bun run build + - name: Verify packed artifact + if: steps.version-check.outputs.changed == 'true' + run: | + npm pack --dry-run --json > "$RUNNER_TEMP/ai-sdk-pack.json" + jq -e ' + (.[0].files | any(.path == "dist/index.js")) and + (.[0].files | any(.path == "dist/index.d.ts")) + ' "$RUNNER_TEMP/ai-sdk-pack.json" >/dev/null + - name: Publish if: steps.version-check.outputs.changed == 'true' run: npm publish --access public --provenance diff --git a/bun.lock b/bun.lock index 875d03d1..c50078de 100644 --- a/bun.lock +++ b/bun.lock @@ -258,7 +258,7 @@ }, "packages/ai-sdk": { "name": "@supermemory/ai-sdk", - "version": "1.0.9", + "version": "2.0.0", "dependencies": { "@ai-sdk/openai": "^2.0.22", "@ai-sdk/provider": "^2.0.0", diff --git a/packages/ai-sdk/README.md b/packages/ai-sdk/README.md index 0927cfdf..fbaf0d25 100644 --- a/packages/ai-sdk/README.md +++ b/packages/ai-sdk/README.md @@ -19,7 +19,7 @@ yarn add @supermemory/ai-sdk Choose **one** of the following approaches (they cannot be used together): - **Infinite Chat Provider**: Connect to various LLM providers with unlimited context support -- **Memory Tools**: Search, add, and fetch memories from supermemory using AI agents +- **Memory Tools**: Search, add, inspect, and manage Supermemory data using AI agents ## Infinite Chat Provider @@ -27,6 +27,7 @@ The infinite chat provider allows you to connect to various LLM providers with s ```typescript import { generateText } from 'ai' +import { createOpenAI } from '@ai-sdk/openai' // Using a custom provider URL const supermemoryOpenai = createOpenAI({ @@ -50,6 +51,7 @@ const result = await generateText({ ```typescript import { generateText } from 'ai' +import { createOpenAI } from '@ai-sdk/openai' const supermemoryApiKey = process.env.SUPERMEMORY_API_KEY! const openaiApiKey = process.env.OPENAI_API_KEY! @@ -104,11 +106,12 @@ interface ConfigWithProviderUrl { ## Memory Tools -supermemory tools allow AI agents to interact with user memories for enhanced context and personalization. +Supermemory tools allow AI agents to search, add, inspect, and manage scoped Supermemory data. ```typescript import { supermemoryTools } from '@supermemory/ai-sdk' import { generateText } from 'ai' +import { openai } from '@ai-sdk/openai' const result = await generateText({ model: openai('gpt-5'), @@ -117,19 +120,15 @@ const result = await generateText({ ], tools: { ...supermemoryTools('your-supermemory-api-key', { - // Optional: specify a base URL for self-hosted instances - baseUrl: 'https://api.supermemory.com', - - // Use either projectId OR containerTags, not both - projectId: 'your-project-id', - // OR - containerTags: ['tag1', 'tag2'] -}), -// Your other tools go here + // Use either projectId OR containerTags, not both. + containerTags: ['user-123'] + }) } }) ``` +> **Important:** `supermemoryTools()` includes destructive operations: `documentDelete` permanently deletes a source document, while `memoryForget` soft-forgets an extracted profile memory. Do not expose the complete aggregate to an agent unless it should be allowed to perform those operations. + ### Complete Memory Tools Example ```typescript @@ -157,7 +156,6 @@ async function chatWithTools(userMessage: string) { containerTags: ['my-user-id'] }) }, - maxToolRoundtrips: 5 }) return result.text @@ -167,18 +165,32 @@ async function chatWithTools(userMessage: string) { ### Configuration ```typescript -interface SupermemoryConfig { - // Optional: Base URL for API calls (default: https://api.supermemory.com) +interface SupermemoryToolsConfig { + // Optional API base URL (default: https://api.supermemory.ai) baseUrl?: string - // Container tags for organizing memories (cannot be used with projectId) + // One or more non-empty scope tags (cannot be used with projectId) containerTags?: string[] - // Project ID for scoping memories (cannot be used with containerTags) + // Converted to sm_project_ (cannot be used with containerTags) projectId?: string + + // Enable the package's stricter provider-compatible input schemas + // (default: false) + strict?: boolean } ``` +`projectId` and `containerTags` are mutually exclusive and empty values are rejected. If neither is provided, v2 uses the explicit scope `sm_project_default`. With multiple `containerTags`, operations that support a union use all configured tags; single-profile operations default to the first tag. + +In strict mode, fields covered by a strict schema are required or defaulted. For example, `documentDelete.containerTag` must be a string or `null`; pass `null` to use the configured scope. + +### Migrating from v1 + +Version 1 returned only `searchMemories` and `addMemory` from `supermemoryTools()`. Version 2 returns all seven tools listed below, including deletion and forgetting, so review any code that spreads the aggregate directly into an agent. + +Version 1 also left `containerTags` undefined when no scope was configured. Version 2 sends `['sm_project_default']` instead. Before upgrading, choose an explicit `projectId` or `containerTags`, or migrate data that should live in the new default scope. + ### Self-Hosted supermemory If you're running a self-hosted supermemory instance: @@ -192,35 +204,17 @@ const tools = supermemoryTools('your-api-key', { ### Available Tools -##### Search Memories +| Aggregate key | Individual creator | Purpose | +| --- | --- | --- | +| `searchMemories` | `searchMemoriesTool` | Search stored source documents | +| `addMemory` | `addMemoryTool` | Add a short, atomic memory | +| `getProfile` | `getProfileTool` | Read static/dynamic profile text and optional query results | +| `documentList` | `documentListTool` | List paginated source-document metadata | +| `documentDelete` | `documentDeleteTool` | Permanently delete a source and soft-forget its extracted memories | +| `documentAdd` | `documentAddTool` | Ingest a source document for asynchronous processing | +| `memoryForget` | `memoryForgetTool` | Soft-forget one extracted profile memory | -Search through user memories using semantic matching. - -```typescript -const searchResult = await tools.searchMemories.execute({ - informationToGet: 'user preferences about coffee' -}) -``` - -##### Add Memory - -Add new memories to the user's memory store. - -```typescript -const addResult = await tools.addMemory.execute({ - memory: 'User prefers dark roast coffee in the morning' -}) -``` - -##### Fetch Memory - -Retrieve a specific memory by its ID. - -```typescript -const fetchResult = await tools.fetchMemory.execute({ - memoryId: 'memory-id-123' -}) -``` +There is no `fetchMemory` or `fetchMemoryTool`. Use `getProfile` for profile memories, `searchMemories` for relevant source content, and `documentList` for source-document IDs and metadata. ### Using Individual Tools @@ -230,7 +224,11 @@ For more flexibility, you can import and use individual tools: import { searchMemoriesTool, addMemoryTool, - fetchMemoryTool + getProfileTool, + documentListTool, + documentDeleteTool, + documentAddTool, + memoryForgetTool } from '@supermemory/ai-sdk' const searchTool = searchMemoriesTool('your-api-key', { @@ -247,6 +245,22 @@ const result = await generateText({ }) ``` +To expose a non-destructive subset, create the aggregate once and select only the tools the agent needs: + +```typescript +const allTools = supermemoryTools('your-api-key', { + containerTags: ['user-123'] +}) + +const safeTools = { + searchMemories: allTools.searchMemories, + addMemory: allTools.addMemory, + getProfile: allTools.getProfile, + documentList: allTools.documentList, + documentAdd: allTools.documentAdd +} +``` + ### Error Handling All tool executions return a result object with a `success` field: @@ -269,33 +283,24 @@ if (result.success) { ### Running Tests ```bash -# Run all tests -bun test +# From the repository root +bun run --cwd packages/ai-sdk test:unit -# Run tests in watch mode -bun test --watch +# Or from packages/ai-sdk +bun run test:unit ``` #### Environment Variables for Tests -All tests require API keys to run. Copy `.env.example` to `.env` and set the required values: +Local initialization and unit checks do not require API keys. Network integration checks run only when both of these are set; otherwise they are skipped: -```bash -cp .env.example .env -``` - -**Required:** -- `SUPERMEMORY_API_KEY`: Your Supermemory API key -- `PROVIDER_API_KEY`: Your AI provider API key (OpenAI, Anthropic, etc.) -- `OPENAI_API_KEY`: Your OpenAI API key for tool integration tests +- `SUPERMEMORY_API_KEY`: Supermemory API key +- `OPENAI_API_KEY`: OpenAI API key **Optional:** -- `SUPERMEMORY_BASE_URL`: Custom Supermemory base URL (defaults to `https://api.supermemory.ai`) -- `PROVIDER_NAME`: Provider name (defaults to `openai`) - one of: `openai`, `anthropic`, `openrouter`, `deepinfra`, `groq`, `google`, `cloudflare` -- `PROVIDER_URL`: Custom provider URL (use instead of `PROVIDER_NAME`) -- `MODEL_NAME`: Model to use in tests (defaults to `gpt-3.5-turbo`) -Tests will fail if required API keys are not provided. +- `SUPERMEMORY_BASE_URL`: Custom Supermemory base URL +- `MODEL_NAME`: OpenAI model used by integration checks (defaults to `gpt-5-nano`) ## License diff --git a/packages/ai-sdk/package.json b/packages/ai-sdk/package.json index d4923c5a..1d43283c 100644 --- a/packages/ai-sdk/package.json +++ b/packages/ai-sdk/package.json @@ -1,19 +1,19 @@ { "name": "@supermemory/ai-sdk", "type": "module", - "version": "1.0.9", + "version": "2.0.0", "scripts": { "build": "tsdown", "dev": "tsdown --watch --ignore-watch .turbo", "check-types": "tsc --noEmit", "test": "vitest", - "test:unit": "vitest run src/tools.unit.test.ts", + "test:unit": "vitest run src/tools.test.ts", "test:watch": "vitest --watch" }, "dependencies": { "@ai-sdk/openai": "^2.0.22", "@ai-sdk/provider": "^2.0.0", - "@supermemory/tools": "workspace:*", + "@supermemory/tools": "^2.2.0", "ai": "^5.0.113", "supermemory": "^4.25.4" }, @@ -24,9 +24,12 @@ "typescript": "^5.9.2", "vitest": "^3.2.4" }, + "files": [ + "dist" + ], "main": "./dist/index.js", "module": "./dist/index.js", - "types": "./dist/index-B8qmWxBg.d.ts", + "types": "./dist/index.d.ts", "exports": { ".": "./dist/index.js", "./package.json": "./package.json" diff --git a/packages/ai-sdk/src/tools.test.ts b/packages/ai-sdk/src/tools.test.ts index 83e397ca..f6e8b41d 100644 --- a/packages/ai-sdk/src/tools.test.ts +++ b/packages/ai-sdk/src/tools.test.ts @@ -5,25 +5,15 @@ import { type SupermemoryToolsConfig, supermemoryTools } from "./tools" import "dotenv/config" -describe.skipIf( - !process.env.SUPERMEMORY_API_KEY || !process.env.OPENAI_API_KEY, -)("supermemoryTools", () => { - // Required API keys — suite is skipped in CI without them - const testApiKey = process.env.SUPERMEMORY_API_KEY as string - const testOpenAIKey = process.env.OPENAI_API_KEY as string - - // Optional configuration with defaults - const testBaseUrl = process.env.SUPERMEMORY_BASE_URL ?? undefined - const testModelName = process.env.MODEL_NAME || "gpt-5-nano" - - const testPrompts = [ - "What do you remember about my preferences?", - "Help me plan my day based on what you know about me", - "What are my current projects?", - "Remind me of my interests and hobbies", - "What should I focus on today?", - ] +const hasIntegrationKeys = Boolean( + process.env.SUPERMEMORY_API_KEY && process.env.OPENAI_API_KEY, +) +const testApiKey = process.env.SUPERMEMORY_API_KEY ?? "test-api-key" +const testOpenAIKey = process.env.OPENAI_API_KEY ?? "test-openai-key" +const testBaseUrl = process.env.SUPERMEMORY_BASE_URL ?? undefined +const testModelName = process.env.MODEL_NAME || "gpt-5-nano" +describe("supermemoryTools", () => { describe("client initialization", () => { it("should create tools with default configuration", () => { const config: SupermemoryToolsConfig = {} @@ -73,7 +63,7 @@ describe.skipIf( }) }) - describe("AI SDK integration", () => { + describe.skipIf(!hasIntegrationKeys)("AI SDK integration", () => { it("should work with AI SDK generateText", async () => { const openai = createOpenAI({ apiKey: testOpenAIKey, @@ -89,7 +79,7 @@ describe.skipIf( }, { role: "user", - content: testPrompts[0]!, + content: "What do you remember about my preferences?", }, ], tools: { diff --git a/packages/ai-sdk/src/tools.unit.test.ts b/packages/ai-sdk/src/tools.unit.test.ts deleted file mode 100644 index e4f58b7a..00000000 --- a/packages/ai-sdk/src/tools.unit.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from "vitest" -import { getContainerTags } from "./tools" - -describe("getContainerTags", () => { - it("defaults to the default project when no config is provided", () => { - expect(getContainerTags()).toEqual(["sm_project_default"]) - }) - - it("converts projectId into a project container tag", () => { - expect(getContainerTags({ projectId: "abc" })).toEqual(["sm_project_abc"]) - }) - - it("uses explicit container tags", () => { - expect(getContainerTags({ containerTags: ["tag-a", "tag-b"] })).toEqual([ - "tag-a", - "tag-b", - ]) - }) - - it("rejects config with both projectId and containerTags", () => { - expect(() => - getContainerTags({ - projectId: "abc", - containerTags: ["tag-a"], - }), - ).toThrow("either projectId or containerTags") - }) -}) diff --git a/packages/ai-sdk/tsdown.config.ts b/packages/ai-sdk/tsdown.config.ts index f587b211..79b3eff1 100644 --- a/packages/ai-sdk/tsdown.config.ts +++ b/packages/ai-sdk/tsdown.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ target: "es2020", tsconfig: "./tsconfig.json", clean: true, + hash: false, minify: true, dts: { sourcemap: true,