From c2c1b0243bee530f6767ba176bcbd007725a7d76 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:18:26 +0530 Subject: [PATCH 1/3] feat(tools,docs): add full export and list API helpers for memories (#1653) --- apps/docs/docs.json | 1 + apps/docs/self-hosting/backup-and-export.mdx | 213 ++++++++++++++++ apps/docs/self-hosting/overview.mdx | 5 +- packages/tools/src/export-memories.test.ts | 250 +++++++++++++++++++ packages/tools/src/index.ts | 15 ++ packages/tools/src/shared/export-memories.ts | 230 +++++++++++++++++ packages/tools/src/shared/index.ts | 16 ++ 7 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 apps/docs/self-hosting/backup-and-export.mdx create mode 100644 packages/tools/src/export-memories.test.ts create mode 100644 packages/tools/src/shared/export-memories.ts diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 75d91754..c11017b1 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -185,6 +185,7 @@ "self-hosting/quickstart", "self-hosting/configuration", "self-hosting/embeddings", + "self-hosting/backup-and-export", "self-hosting/providers", "self-hosting/local-vs-enterprise" ] diff --git a/apps/docs/self-hosting/backup-and-export.mdx b/apps/docs/self-hosting/backup-and-export.mdx new file mode 100644 index 00000000..d217f3aa --- /dev/null +++ b/apps/docs/self-hosting/backup-and-export.mdx @@ -0,0 +1,213 @@ +--- +title: "Backup & Export" +sidebarTitle: "Backup & Export" +description: "How to export and back up distilled memories and documents from your self-hosted instance." +icon: "download" +--- + +Self-hosted Supermemory stores all state inside `$SUPERMEMORY_DATA_DIR` (default: `./.supermemory`). +This guide explains how to back up and export both your **distilled memory cards** (extracted facts) and **raw ingested documents**. + +## Overview: Memories vs Documents + +Supermemory distinguishes between two layers of context: + +1. **Documents (`/v3/documents`)**: The raw source material you ingested (chat logs, transcripts, Markdown files, PDFs, web pages). +2. **Extracted Memories (`/v4/memories`)**: The distilled, atomic facts extracted from documents, complete with version history, temporal status, and container tags. + +> **Why not copy `./.supermemory/data` directly?** +> The local store uses an encrypted PGlite container (`SMD1` format) keyed to the machine environment. To produce portable, human-readable backups or migrate between machines, use the HTTP export APIs described below. + +--- + +## Exporting Extracted Memories + +To export the distilled memory cards for any container tag, use the `POST /v4/memories/list` endpoint. Unlike search recall (`searchMode: "memories"`), which only returns top-K semantically matching items, `/v4/memories/list` provides **exhaustive, paginated access** to all extracted facts. + +### Endpoint + +```http +POST /v4/memories/list +Content-Type: application/json +Authorization: Bearer +``` + +#### Request Payload + +```json +{ + "containerTags": ["user_123"], + "page": 1, + "limit": 50, + "sort": "createdAt", + "order": "desc" +} +``` + +#### Response Structure + +```json +{ + "memoryEntries": [ + { + "id": "mem_01J6ABC...", + "memory": "User prefers TypeScript and strict mode enabled", + "version": 1, + "isLatest": true, + "isForgotten": false, + "isStatic": true, + "createdAt": "2026-08-20T10:00:00.000Z", + "updatedAt": "2026-08-20T10:00:00.000Z", + "documentIds": ["doc_01J6XYZ..."], + "history": [] + } + ], + "pagination": { + "currentPage": 1, + "limit": 50, + "totalItems": 142, + "totalPages": 3 + } +} +``` + +--- + +## Full Export Scripts + +### 1. Using `@supermemory/tools` (Node.js / Bun) + +If you use `@supermemory/tools`, use the built-in export helpers: + +```typescript +import { + exportMemoriesAsJson, + exportMemoriesAsMarkdown, + fetchAllMemories, +} from "@supermemory/tools"; + +const options = { + baseUrl: "http://localhost:6767", + apiKey: process.env.SUPERMEMORY_API_KEY || "sm_...", +}; + +// Export to JSON backup +const jsonBackup = await exportMemoriesAsJson("user_123", options); +await Bun.write("memories-backup.json", jsonBackup); + +// Export to human-readable Markdown +const markdownNotes = await exportMemoriesAsMarkdown("user_123", options); +await Bun.write("memories-export.md", markdownNotes); +``` + +### 2. Standalone Python Export Script + +Run this script against your local server to export all memories to JSON: + +```python +import os +import json +import requests + +BASE_URL = os.getenv("SUPERMEMORY_API_URL", "http://localhost:6767") +API_KEY = os.getenv("SUPERMEMORY_API_KEY", "") +CONTAINER_TAG = "user_123" + +def export_all_memories(container_tag): + headers = { + "Authorization": f"Bearer {API_KEY}", + "Content-Type": "application/json" + } + + page = 1 + limit = 50 + all_memories = [] + + while True: + payload = { + "containerTags": [container_tag], + "page": page, + "limit": limit, + "sort": "createdAt", + "order": "desc" + } + res = requests.post(f"{BASE_URL}/v4/memories/list", json=payload, headers=headers) + res.raise_for_status() + data = res.json() + + entries = data.get("memoryEntries", []) + all_memories.extend(entries) + + pagination = data.get("pagination", {}) + total_pages = pagination.get("totalPages", 1) + + print(f"Fetched page {page} / {total_pages} ({len(entries)} entries)") + if page >= total_pages or not entries: + break + page += 1 + + return all_memories + +if __name__ == "__main__": + memories = export_all_memories(CONTAINER_TAG) + output_file = f"memories_{CONTAINER_TAG}.json" + with open(output_file, "w", encoding="utf-8") as f: + json.dump({ + "containerTag": CONTAINER_TAG, + "totalCount": len(memories), + "memories": memories + }, f, indent=2) + print(f"Exported {len(memories)} memories to {output_file}") +``` + +### 3. Quick cURL Export + +```bash +curl -s -X POST "http://localhost:6767/v4/memories/list" \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "containerTags": ["user_123"], + "page": 1, + "limit": 50 + }' | jq . +``` + +--- + +## Exporting Raw Documents + +To back up the underlying conversation sessions, files, and text documents: + +```bash +curl -s "http://localhost:6767/v3/documents?containerTag=user_123&page=1&limit=50" \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" | jq . +``` + +To fetch the full text of a specific document: + +```bash +curl -s "http://localhost:6767/v3/documents/{documentId}" \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" | jq . +``` + +--- + +## Migration to Hosted Platform + +To migrate your exported memories from a local server to the hosted platform: + +1. Export your memory cards using the scripts above. +2. Direct-write the memories into the hosted platform via `POST /v4/memories`: + +```bash +curl -X POST "https://api.supermemory.ai/v4/memories" \ + -H "Authorization: Bearer $SUPERMEMORY_CLOUD_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "memories": [ + { "content": "User prefers TypeScript and strict mode enabled", "isStatic": true } + ], + "containerTag": "user_123" + }' +``` diff --git a/apps/docs/self-hosting/overview.mdx b/apps/docs/self-hosting/overview.mdx index 8fc9fe23..11460950 100644 --- a/apps/docs/self-hosting/overview.mdx +++ b/apps/docs/self-hosting/overview.mdx @@ -75,7 +75,7 @@ If you outgrow a single machine — or want connectors, MCP, and the best-tuned ## Next steps - + Install, run, and store your first memory in under two minutes @@ -85,4 +85,7 @@ If you outgrow a single machine — or want connectors, MCP, and the best-tuned Local default, remote providers, multilingual, dimension lock + + Export and back up distilled memories and documents + diff --git a/packages/tools/src/export-memories.test.ts b/packages/tools/src/export-memories.test.ts new file mode 100644 index 00000000..3fd20aa1 --- /dev/null +++ b/packages/tools/src/export-memories.test.ts @@ -0,0 +1,250 @@ +import { describe, expect, it } from "bun:test" +import { + exportMemoriesAsJson, + exportMemoriesAsMarkdown, + fetchAllMemories, + listMemoriesRequest, + type MemoriesListResponse, +} from "./shared/export-memories" + +function createMockFetch(responses: MemoriesListResponse[]) { + let callCount = 0 + const fetchMock = async (url: string | URL | Request, init?: RequestInit) => { + const pageResponse = responses[callCount] || { + memoryEntries: [], + pagination: { currentPage: callCount + 1, limit: 10, totalItems: 0, totalPages: 1 }, + } + callCount++ + + return { + ok: true, + status: 200, + statusText: "OK", + json: async () => pageResponse, + text: async () => JSON.stringify(pageResponse), + } as Response + } + return { + fetchFn: fetchMock as unknown as typeof fetch, + getCallCount: () => callCount, + } +} + +describe("listMemoriesRequest", () => { + it("sends correct POST body and headers to /v4/memories/list", async () => { + let capturedUrl = "" + let capturedInit: RequestInit | undefined + + const customFetch = async (url: string | URL | Request, init?: RequestInit) => { + capturedUrl = String(url) + capturedInit = init + return { + ok: true, + status: 200, + json: async () => ({ + memoryEntries: [ + { + id: "mem_1", + memory: "User prefers TypeScript", + version: 1, + isLatest: true, + isForgotten: false, + createdAt: "2026-09-01T10:00:00.000Z", + updatedAt: "2026-09-01T10:00:00.000Z", + }, + ], + pagination: { currentPage: 1, limit: 10, totalItems: 1, totalPages: 1 }, + }), + } as Response + } + + const result = await listMemoriesRequest( + "sm_test_key", + { containerTags: ["user_123"], page: 1, limit: 10 }, + "http://localhost:6767", + { fetchFn: customFetch as unknown as typeof fetch }, + ) + + expect(capturedUrl).toBe("http://localhost:6767/v4/memories/list") + expect(capturedInit?.method).toBe("POST") + expect((capturedInit?.headers as Record)?.Authorization).toBe( + "Bearer sm_test_key", + ) + const parsedBody = JSON.parse(String(capturedInit?.body)) + expect(parsedBody.containerTags).toEqual(["user_123"]) + expect(parsedBody.page).toBe(1) + expect(result.memoryEntries).toHaveLength(1) + expect(result.memoryEntries[0].memory).toBe("User prefers TypeScript") + }) + + it("throws on non-200 responses with descriptive error", async () => { + const customFetch = async () => + ({ + ok: false, + status: 400, + statusText: "Bad Request", + text: async () => JSON.stringify({ error: "Container tag is required" }), + }) as unknown as Response + + expect( + listMemoriesRequest( + "sm_key", + { containerTags: [] }, + "http://localhost:6767", + { fetchFn: customFetch as unknown as typeof fetch }, + ), + ).rejects.toThrow("Supermemory list memories failed: 400 Bad Request") + }) +}) + +describe("fetchAllMemories", () => { + it("paginates through all pages until totalPages is reached", async () => { + const page1: MemoriesListResponse = { + memoryEntries: [ + { + id: "mem_1", + memory: "Fact 1", + version: 1, + isLatest: true, + isForgotten: false, + createdAt: "2026-09-01T10:00:00.000Z", + updatedAt: "2026-09-01T10:00:00.000Z", + }, + { + id: "mem_2", + memory: "Fact 2 (forgotten)", + version: 1, + isLatest: true, + isForgotten: true, + createdAt: "2026-09-01T11:00:00.000Z", + updatedAt: "2026-09-01T11:00:00.000Z", + }, + ], + pagination: { currentPage: 1, limit: 2, totalItems: 3, totalPages: 2 }, + } + + const page2: MemoriesListResponse = { + memoryEntries: [ + { + id: "mem_3", + memory: "Fact 3", + version: 1, + isLatest: true, + isForgotten: false, + createdAt: "2026-09-01T12:00:00.000Z", + updatedAt: "2026-09-01T12:00:00.000Z", + }, + ], + pagination: { currentPage: 2, limit: 2, totalItems: 3, totalPages: 2 }, + } + + const { fetchFn, getCallCount } = createMockFetch([page1, page2]) + const memories = await fetchAllMemories("user_123", { + baseUrl: "http://localhost:6767", + apiKey: "sm_key", + fetchFn, + }) + + expect(getCallCount()).toBe(2) + // By default forgotten memories are excluded + expect(memories).toHaveLength(2) + expect(memories[0].id).toBe("mem_1") + expect(memories[1].id).toBe("mem_3") + }) + + it("includes forgotten memories when includeForgotten is true", async () => { + const page1: MemoriesListResponse = { + memoryEntries: [ + { + id: "mem_1", + memory: "Fact 1", + version: 1, + isLatest: true, + isForgotten: false, + createdAt: "2026-09-01T10:00:00.000Z", + updatedAt: "2026-09-01T10:00:00.000Z", + }, + { + id: "mem_2", + memory: "Fact 2 (forgotten)", + version: 1, + isLatest: true, + isForgotten: true, + createdAt: "2026-09-01T11:00:00.000Z", + updatedAt: "2026-09-01T11:00:00.000Z", + }, + ], + pagination: { currentPage: 1, limit: 2, totalItems: 2, totalPages: 1 }, + } + + const { fetchFn } = createMockFetch([page1]) + const memories = await fetchAllMemories("user_123", { + includeForgotten: true, + fetchFn, + }) + + expect(memories).toHaveLength(2) + expect(memories[1].id).toBe("mem_2") + expect(memories[1].isForgotten).toBe(true) + }) +}) + +describe("exportMemoriesAsJson and exportMemoriesAsMarkdown", () => { + const sampleData: MemoriesListResponse = { + memoryEntries: [ + { + id: "mem_abc123", + memory: "User works at Acme Corp", + version: 2, + isLatest: true, + isForgotten: false, + isStatic: true, + createdAt: "2026-08-01T00:00:00.000Z", + updatedAt: "2026-08-10T00:00:00.000Z", + documentIds: ["doc_1", "doc_2"], + history: [ + { + id: "mem_abc122", + memory: "User works at Startup", + version: 1, + createdAt: "2026-07-01T00:00:00.000Z", + updatedAt: "2026-07-01T00:00:00.000Z", + }, + ], + }, + ], + pagination: { currentPage: 1, limit: 50, totalItems: 1, totalPages: 1 }, + } + + it("exports memories as valid JSON with backup metadata", async () => { + const { fetchFn } = createMockFetch([sampleData]) + const jsonString = await exportMemoriesAsJson("user_123", { + baseUrl: "http://localhost:6767", + fetchFn, + }) + + const parsed = JSON.parse(jsonString) + expect(parsed.containerTag).toBe("user_123") + expect(parsed.totalCount).toBe(1) + expect(parsed.baseUrl).toBe("http://localhost:6767") + expect(parsed.memories).toHaveLength(1) + expect(parsed.memories[0].id).toBe("mem_abc123") + expect(parsed.memories[0].memory).toBe("User works at Acme Corp") + }) + + it("exports memories as clean Markdown format", async () => { + const { fetchFn } = createMockFetch([sampleData]) + const markdown = await exportMemoriesAsMarkdown("user_123", { + baseUrl: "http://localhost:6767", + fetchFn, + }) + + expect(markdown).toContain("# Supermemory Backup: user_123") + expect(markdown).toContain("Total Memories:** 1") + expect(markdown).toContain("### 1. User works at Acme Corp") + expect(markdown).toContain("- **ID:** `mem_abc123`") + expect(markdown).toContain("- **Type:** Static") + expect(markdown).toContain("- **Source Documents:** `doc_1`, `doc_2`") + expect(markdown).toContain("- **Previous Revisions:** 1") + }) +}) diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index e7bef409..ba52db4c 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -10,3 +10,18 @@ export { DEFAULT_VALUES, getContainerTags, } from "./tools-shared" + +export { + listMemoriesRequest, + fetchAllMemories, + exportMemoriesAsJson, + exportMemoriesAsMarkdown, + type MemoryEntry, + type MemoryEntryHistory, + type MemoriesListResponse, + type ListMemoriesParams, + type ListMemoriesRequestOptions, + type ExportMemoriesOptions, + type MemoriesExportData, +} from "./shared" + diff --git a/packages/tools/src/shared/export-memories.ts b/packages/tools/src/shared/export-memories.ts new file mode 100644 index 00000000..4a806373 --- /dev/null +++ b/packages/tools/src/shared/export-memories.ts @@ -0,0 +1,230 @@ +const DEFAULT_BASE_URL = "https://api.supermemory.ai" +const FETCH_TIMEOUT_MS = 30_000 +const DEFAULT_PAGE_SIZE = 50 + +export interface MemoryEntryHistory { + id: string + memory: string + version: number + createdAt: string + updatedAt: string + parentMemoryId?: string | null + rootMemoryId?: string | null + isLatest?: boolean + isForgotten?: boolean +} + +export interface MemoryEntry { + id: string + memory: string + version: number + isLatest: boolean + isForgotten: boolean + isStatic?: boolean + isInference?: boolean + createdAt: string + updatedAt: string + sourceCount?: number + documentIds?: string[] + history?: MemoryEntryHistory[] +} + +export interface MemoriesListResponse { + memoryEntries: MemoryEntry[] + pagination: { + currentPage: number + limit: number + totalItems: number + totalPages: number + } +} + +export interface ListMemoriesParams { + containerTags: string[] + page?: number + limit?: number + sort?: "createdAt" | "updatedAt" + order?: "asc" | "desc" + filters?: unknown +} + +export interface ListMemoriesRequestOptions { + signal?: AbortSignal + fetchFn?: typeof fetch +} + +export interface ExportMemoriesOptions { + baseUrl?: string + apiKey?: string + pageSize?: number + maxPages?: number + includeForgotten?: boolean + signal?: AbortSignal + fetchFn?: typeof fetch +} + +export interface MemoriesExportData { + containerTag: string + exportedAt: string + totalCount: number + baseUrl: string + memories: MemoryEntry[] +} + +/** + * Fetch a page of memory entries directly from the `/v4/memories/list` endpoint. + * Works with both hosted platform (`https://api.supermemory.ai`) and + * self-hosted Supermemory server (`http://localhost:6767`). + */ +export async function listMemoriesRequest( + apiKey: string, + params: ListMemoriesParams, + baseUrl: string = DEFAULT_BASE_URL, + options?: ListMemoriesRequestOptions, +): Promise { + const customFetch = options?.fetchFn ?? fetch + const cleanBase = baseUrl.replace(/\/+$/, "") + const response = await customFetch(`${cleanBase}/v4/memories/list`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "x-sm-source": "tools-export", + }, + body: JSON.stringify({ + containerTags: params.containerTags, + page: params.page ?? 1, + limit: params.limit ?? DEFAULT_PAGE_SIZE, + sort: params.sort ?? "createdAt", + order: params.order ?? "desc", + ...(params.filters ? { filters: params.filters } : {}), + }), + signal: options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS), + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => "Unknown error") + throw new Error( + `Supermemory list memories failed: ${response.status} ${response.statusText}. ${errorText}`, + ) + } + + return (await response.json()) as MemoriesListResponse +} + +/** + * Iteratively fetch all memory entries for a specific containerTag across all pages. + */ +export async function fetchAllMemories( + containerTag: string, + options?: ExportMemoriesOptions, +): Promise { + const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL + const apiKey = options?.apiKey ?? "" + const limit = options?.pageSize ?? DEFAULT_PAGE_SIZE + const maxPages = options?.maxPages ?? 1000 + const includeForgotten = options?.includeForgotten ?? false + + const allMemories: MemoryEntry[] = [] + let currentPage = 1 + let hasMore = true + + while (hasMore && currentPage <= maxPages) { + const response = await listMemoriesRequest( + apiKey, + { + containerTags: [containerTag], + page: currentPage, + limit, + sort: "createdAt", + order: "desc", + }, + baseUrl, + { + signal: options?.signal, + fetchFn: options?.fetchFn, + }, + ) + + const entries = response.memoryEntries ?? [] + for (const entry of entries) { + if (!includeForgotten && entry.isForgotten) { + continue + } + allMemories.push(entry) + } + + const totalPages = response.pagination?.totalPages ?? 1 + if (currentPage >= totalPages || entries.length === 0) { + hasMore = false + } else { + currentPage++ + } + } + + return allMemories +} + +/** + * Export all memories for a container tag formatted as JSON with backup metadata. + */ +export async function exportMemoriesAsJson( + containerTag: string, + options?: ExportMemoriesOptions, +): Promise { + const memories = await fetchAllMemories(containerTag, options) + const exportData: MemoriesExportData = { + containerTag, + exportedAt: new Date().toISOString(), + totalCount: memories.length, + baseUrl: options?.baseUrl ?? DEFAULT_BASE_URL, + memories, + } + return JSON.stringify(exportData, null, 2) +} + +/** + * Export all memories for a container tag formatted as human-readable Markdown. + */ +export async function exportMemoriesAsMarkdown( + containerTag: string, + options?: ExportMemoriesOptions, +): Promise { + const memories = await fetchAllMemories(containerTag, options) + const lines: string[] = [ + `# Supermemory Backup: ${containerTag}`, + "", + `- **Exported At:** ${new Date().toISOString()}`, + `- **Total Memories:** ${memories.length}`, + `- **Server:** ${options?.baseUrl ?? DEFAULT_BASE_URL}`, + "", + "---", + "", + ] + + if (memories.length === 0) { + lines.push("_No memories found for this container tag._") + return lines.join("\n") + } + + for (let i = 0; i < memories.length; i++) { + const m = memories[i] + const status = m.isForgotten ? " [FORGOTTEN]" : "" + const type = m.isStatic ? "Static" : "Dynamic" + lines.push(`### ${i + 1}. ${m.memory}${status}`) + lines.push(`- **ID:** \`${m.id}\``) + lines.push(`- **Type:** ${type}`) + lines.push(`- **Version:** ${m.version}`) + lines.push(`- **Created:** ${m.createdAt}`) + lines.push(`- **Updated:** ${m.updatedAt}`) + if (m.documentIds && m.documentIds.length > 0) { + lines.push(`- **Source Documents:** ${m.documentIds.map((d) => `\`${d}\``).join(", ")}`) + } + if (m.history && m.history.length > 0) { + lines.push(`- **Previous Revisions:** ${m.history.length}`) + } + lines.push("") + } + + return lines.join("\n") +} diff --git a/packages/tools/src/shared/index.ts b/packages/tools/src/shared/index.ts index 602d9fe3..943e0c18 100644 --- a/packages/tools/src/shared/index.ts +++ b/packages/tools/src/shared/index.ts @@ -51,3 +51,19 @@ export { wrapMemoryContext, replaceMemoryContext, } from "./memory-context" + +// Memory listing and export +export { + listMemoriesRequest, + fetchAllMemories, + exportMemoriesAsJson, + exportMemoriesAsMarkdown, + type MemoryEntry, + type MemoryEntryHistory, + type MemoriesListResponse, + type ListMemoriesParams, + type ListMemoriesRequestOptions, + type ExportMemoriesOptions, + type MemoriesExportData, +} from "./export-memories" + From adc9019eab5bdbe87ac0570a6497db048bce0429 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:24:46 +0530 Subject: [PATCH 2/3] feat(tools,docs): add supermemory-export CLI and documentation --- apps/docs/self-hosting/backup-and-export.mdx | 23 +++- packages/tools/bin/supermemory-export.ts | 125 +++++++++++++++++++ packages/tools/package.json | 6 +- 3 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 packages/tools/bin/supermemory-export.ts diff --git a/apps/docs/self-hosting/backup-and-export.mdx b/apps/docs/self-hosting/backup-and-export.mdx index d217f3aa..57184fa7 100644 --- a/apps/docs/self-hosting/backup-and-export.mdx +++ b/apps/docs/self-hosting/backup-and-export.mdx @@ -75,9 +75,24 @@ Authorization: Bearer ## Full Export Scripts -### 1. Using `@supermemory/tools` (Node.js / Bun) +### 1. Using the CLI (`supermemory-export`) -If you use `@supermemory/tools`, use the built-in export helpers: +You can export memories directly from your terminal using the CLI packaged with `@supermemory/tools`: + +```bash +# Export to stdout as JSON +bunx @supermemory/tools supermemory-export --tag user_123 + +# Export to a JSON file from local self-hosted server +bunx @supermemory/tools supermemory-export --tag user_123 --out ./memories.json --url http://localhost:6767 + +# Export as formatted Markdown notes +bunx @supermemory/tools supermemory-export --tag user_123 --format markdown --out ./memories.md +``` + +### 2. Programmatic Export (TypeScript / Bun) + +If you use `@supermemory/tools` in your application: ```typescript import { @@ -100,7 +115,7 @@ const markdownNotes = await exportMemoriesAsMarkdown("user_123", options); await Bun.write("memories-export.md", markdownNotes); ``` -### 2. Standalone Python Export Script +### 3. Standalone Python Export Script Run this script against your local server to export all memories to JSON: @@ -160,7 +175,7 @@ if __name__ == "__main__": print(f"Exported {len(memories)} memories to {output_file}") ``` -### 3. Quick cURL Export +### 4. Quick cURL Export ```bash curl -s -X POST "http://localhost:6767/v4/memories/list" \ diff --git a/packages/tools/bin/supermemory-export.ts b/packages/tools/bin/supermemory-export.ts new file mode 100644 index 00000000..818a4dad --- /dev/null +++ b/packages/tools/bin/supermemory-export.ts @@ -0,0 +1,125 @@ +#!/usr/bin/env bun +/** + * CLI to export extracted memories from Supermemory (self-hosted or hosted platform). + * + * Usage: + * bun run ./bin/supermemory-export.ts --tag user_123 [--format json|markdown] [--out backup.json] [--url http://localhost:6767] + */ + +import { parseArgs } from "node:util" +import { writeFile } from "node:fs/promises" +import { + exportMemoriesAsJson, + exportMemoriesAsMarkdown, +} from "../src/shared/export-memories" + +function printHelp() { + console.log(` +Supermemory Memory Export CLI +============================= +Export extracted memories and facts from Supermemory (self-hosted or cloud platform). + +Usage: + supermemory-export --tag [options] + +Options: + -t, --tag Container tag to export memories from (required) + -f, --format Output format: 'json' (default) or 'markdown' + -o, --out Output file path (default: stdout) + -u, --url Supermemory server base URL (default: $SUPERMEMORY_API_URL or http://localhost:6767) + -k, --key Supermemory API key (default: $SUPERMEMORY_API_KEY) + --include-forgotten Include soft-forgotten memories in export + -h, --help Show this help message + +Examples: + # Export to stdout as JSON + supermemory-export --tag user_123 + + # Export to a JSON backup file from self-hosted server + supermemory-export --tag user_123 --out ./backup.json --url http://localhost:6767 + + # Export as Markdown notes from cloud platform + supermemory-export --tag user_123 --format markdown --out ./notes.md --url https://api.supermemory.ai --key sm_... +`) +} + +async function main() { + let parsed: ReturnType + try { + parsed = parseArgs({ + args: process.argv.slice(2), + options: { + tag: { type: "string", short: "t" }, + format: { type: "string", short: "f", default: "json" }, + out: { type: "string", short: "o" }, + url: { + type: "string", + short: "u", + default: process.env.SUPERMEMORY_API_URL || "http://localhost:6767", + }, + key: { + type: "string", + short: "k", + default: process.env.SUPERMEMORY_API_KEY || "", + }, + "include-forgotten": { type: "boolean", default: false }, + help: { type: "boolean", short: "h" }, + }, + allowPositionals: true, + }) + } catch (err: unknown) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`) + printHelp() + process.exit(1) + } + + const { values } = parsed + + if (values.help) { + printHelp() + process.exit(0) + } + + const tag = values.tag + if (!tag) { + console.error("Error: --tag is required.") + printHelp() + process.exit(1) + } + + const format = values.format?.toLowerCase() || "json" + const baseUrl = values.url || "http://localhost:6767" + const apiKey = values.key || "" + const includeForgotten = Boolean(values["include-forgotten"]) + + try { + let output: string + if (format === "markdown" || format === "md") { + output = await exportMemoriesAsMarkdown(tag, { + baseUrl, + apiKey, + includeForgotten, + }) + } else { + output = await exportMemoriesAsJson(tag, { + baseUrl, + apiKey, + includeForgotten, + }) + } + + if (values.out) { + await writeFile(values.out, output, "utf-8") + console.error(`✓ Successfully exported memories for '${tag}' to ${values.out}`) + } else { + process.stdout.write(output + "\n") + } + } catch (err: unknown) { + console.error( + `Failed to export memories: ${err instanceof Error ? err.message : String(err)}`, + ) + process.exit(1) + } +} + +main() diff --git a/packages/tools/package.json b/packages/tools/package.json index 05f6b6f2..6190ac19 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -41,11 +41,15 @@ "optional": true } }, + "bin": { + "supermemory-export": "./bin/supermemory-export.ts" + }, "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ - "dist" + "dist", + "bin" ], "exports": { ".": "./dist/index.js", From b5722da157d35d9620a561002cc85d7f4f6bf39b Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:31:40 +0530 Subject: [PATCH 3/3] style: apply biome formatting and lint fixes for CI --- packages/tools/bin/supermemory-export.ts | 6 ++-- packages/tools/src/export-memories.test.ts | 33 +++++++++++++++----- packages/tools/src/index.ts | 1 - packages/tools/src/shared/export-memories.ts | 4 ++- packages/tools/src/shared/index.ts | 1 - 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/tools/bin/supermemory-export.ts b/packages/tools/bin/supermemory-export.ts index 818a4dad..c0f149b8 100644 --- a/packages/tools/bin/supermemory-export.ts +++ b/packages/tools/bin/supermemory-export.ts @@ -110,9 +110,11 @@ async function main() { if (values.out) { await writeFile(values.out, output, "utf-8") - console.error(`✓ Successfully exported memories for '${tag}' to ${values.out}`) + console.error( + `✓ Successfully exported memories for '${tag}' to ${values.out}`, + ) } else { - process.stdout.write(output + "\n") + process.stdout.write(`${output}\n`) } } catch (err: unknown) { console.error( diff --git a/packages/tools/src/export-memories.test.ts b/packages/tools/src/export-memories.test.ts index 3fd20aa1..78d289d8 100644 --- a/packages/tools/src/export-memories.test.ts +++ b/packages/tools/src/export-memories.test.ts @@ -9,10 +9,18 @@ import { function createMockFetch(responses: MemoriesListResponse[]) { let callCount = 0 - const fetchMock = async (url: string | URL | Request, init?: RequestInit) => { + const fetchMock = async ( + _url: string | URL | Request, + _init?: RequestInit, + ) => { const pageResponse = responses[callCount] || { memoryEntries: [], - pagination: { currentPage: callCount + 1, limit: 10, totalItems: 0, totalPages: 1 }, + pagination: { + currentPage: callCount + 1, + limit: 10, + totalItems: 0, + totalPages: 1, + }, } callCount++ @@ -35,7 +43,10 @@ describe("listMemoriesRequest", () => { let capturedUrl = "" let capturedInit: RequestInit | undefined - const customFetch = async (url: string | URL | Request, init?: RequestInit) => { + const customFetch = async ( + url: string | URL | Request, + init?: RequestInit, + ) => { capturedUrl = String(url) capturedInit = init return { @@ -53,7 +64,12 @@ describe("listMemoriesRequest", () => { updatedAt: "2026-09-01T10:00:00.000Z", }, ], - pagination: { currentPage: 1, limit: 10, totalItems: 1, totalPages: 1 }, + pagination: { + currentPage: 1, + limit: 10, + totalItems: 1, + totalPages: 1, + }, }), } as Response } @@ -67,9 +83,9 @@ describe("listMemoriesRequest", () => { expect(capturedUrl).toBe("http://localhost:6767/v4/memories/list") expect(capturedInit?.method).toBe("POST") - expect((capturedInit?.headers as Record)?.Authorization).toBe( - "Bearer sm_test_key", - ) + expect( + (capturedInit?.headers as Record)?.Authorization, + ).toBe("Bearer sm_test_key") const parsedBody = JSON.parse(String(capturedInit?.body)) expect(parsedBody.containerTags).toEqual(["user_123"]) expect(parsedBody.page).toBe(1) @@ -83,7 +99,8 @@ describe("listMemoriesRequest", () => { ok: false, status: 400, statusText: "Bad Request", - text: async () => JSON.stringify({ error: "Container tag is required" }), + text: async () => + JSON.stringify({ error: "Container tag is required" }), }) as unknown as Response expect( diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index ba52db4c..f9c59897 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -24,4 +24,3 @@ export { type ExportMemoriesOptions, type MemoriesExportData, } from "./shared" - diff --git a/packages/tools/src/shared/export-memories.ts b/packages/tools/src/shared/export-memories.ts index 4a806373..f706c2cf 100644 --- a/packages/tools/src/shared/export-memories.ts +++ b/packages/tools/src/shared/export-memories.ts @@ -218,7 +218,9 @@ export async function exportMemoriesAsMarkdown( lines.push(`- **Created:** ${m.createdAt}`) lines.push(`- **Updated:** ${m.updatedAt}`) if (m.documentIds && m.documentIds.length > 0) { - lines.push(`- **Source Documents:** ${m.documentIds.map((d) => `\`${d}\``).join(", ")}`) + lines.push( + `- **Source Documents:** ${m.documentIds.map((d) => `\`${d}\``).join(", ")}`, + ) } if (m.history && m.history.length > 0) { lines.push(`- **Previous Revisions:** ${m.history.length}`) diff --git a/packages/tools/src/shared/index.ts b/packages/tools/src/shared/index.ts index 943e0c18..5df5c1a2 100644 --- a/packages/tools/src/shared/index.ts +++ b/packages/tools/src/shared/index.ts @@ -66,4 +66,3 @@ export { type ExportMemoriesOptions, type MemoriesExportData, } from "./export-memories" -