feat(sdk-playground): interactive SDK chat playground (#1437)

## Summary
- Add `apps/sdk-playground` — chat UI to test TS/Python SDK integrations
- Context panel with document memories, API keys in dashboard, tools reference tab
- Python FastAPI server on port 8792; portless entry in `portless.json`

Stacked on #1436

## Test plan
- [ ] `cd apps/sdk-playground && bun run check-types`
- [ ] `bun run dev` with Supermemory + OpenAI keys in UI
- [ ] Switch SDKs and verify chat + context panel

Made with [Cursor](https://cursor.com)
This commit is contained in:
Dhravya 2026-09-01 06:10:36 +00:00
parent 7974498062
commit b01d2b69a3
No known key found for this signature in database
GPG key ID: 135A27003CF4F6CB
29 changed files with 5768 additions and 10 deletions

View file

@ -26,7 +26,7 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Detect SDK package changes
- name: Detect SDK and playground changes
id: sdk-changes
run: |
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- packages/tools; then
@ -41,16 +41,45 @@ jobs:
echo "ai_sdk=true" >> "$GITHUB_OUTPUT"
fi
if git diff --quiet "${{ github.event.pull_request.base.sha }}" HEAD -- apps/sdk-playground; then
echo "sdk_playground=false" >> "$GITHUB_OUTPUT"
else
echo "sdk_playground=true" >> "$GITHUB_OUTPUT"
fi
- name: Setup Python for SDK Playground
if: steps.sdk-changes.outputs.sdk_playground == 'true'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Setup uv for SDK Playground
if: steps.sdk-changes.outputs.sdk_playground == 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"
enable-cache: true
working-directory: apps/sdk-playground/python
cache-dependency-glob: uv.lock
- name: Validate SDK Playground Python server
if: steps.sdk-changes.outputs.sdk_playground == 'true'
working-directory: apps/sdk-playground/python
run: |
uv sync --locked --python 3.12
.venv/bin/python -m py_compile server.py
.venv/bin/python -c "import server"
- name: Run Tools unit tests
if: steps.sdk-changes.outputs.tools == 'true'
run: bun run --cwd packages/tools test:unit
- name: Build Tools package
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true'
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == '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'
if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd packages/ai-sdk check-types
- name: Run AI SDK unit tests
@ -58,9 +87,17 @@ jobs:
run: bun run --cwd packages/ai-sdk test:unit
- name: Build AI SDK package
if: steps.sdk-changes.outputs.ai_sdk == 'true'
if: steps.sdk-changes.outputs.ai_sdk == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd packages/ai-sdk build
- name: Run SDK Playground type checking
if: steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd apps/sdk-playground check-types:app
- name: Build SDK Playground
if: steps.sdk-changes.outputs.sdk_playground == 'true'
run: bun run --cwd apps/sdk-playground build:app
- name: Run Memory Graph type checking
run: bun run --cwd packages/memory-graph check-types

View file

@ -0,0 +1,11 @@
# Local development only. Use disposable development credentials, not production keys.
SUPERMEMORY_API_KEY=
OPENAI_API_KEY=
SUPERMEMORY_BASE_URL=
# Optional
MODEL_NAME=gpt-4o-mini
SDK_PLAYGROUND_PYTHON_URL=http://127.0.0.1:8792
SDK_PLAYGROUND_PYTHON_PORT=8792
# Leave unset unless a trusted non-local development hostname must use env keys.
# SDK_PLAYGROUND_ALLOW_ENV_KEYS=true

7
apps/sdk-playground/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.next
.env*
!.env.example
node_modules
python/.venv
next-env.d.ts
*.tsbuildinfo

View file

@ -0,0 +1,94 @@
# SDK Agent Playground
Chat with a **real agent** and switch which Supermemory SDK integration powers it.
> [!WARNING]
> This is a local, single-user development tool. It makes real API calls, stores
> browser-entered keys only in memory unless you opt into tab-scoped
> `sessionStorage`, and exposes tools that can permanently delete documents. Use
> disposable development credentials and a test container; do not deploy it or
> point it at production data.
## Integrations
| SDK | Style | What happens |
|-----|-------|----------------|
| AI SDK + middleware | automatic | `withSupermemory` injects context + saves chat |
| OpenAI + middleware | automatic | same, via OpenAI client wrapper |
| AI SDK + tools | explicit | model calls 7 memory tools via `generateText` |
| OpenAI + tools | explicit | OpenAI function-calling loop |
| `@supermemory/ai-sdk` | explicit | re-export of tools/ai-sdk |
| Python OpenAI middleware | automatic | `with_supermemory` |
| Python OpenAI tools | explicit | `SupermemoryTools` loop |
| Python supermemory direct | manual | `profile()` + OpenAI + `add()` |
## Setup
Prerequisites: Bun 1.3.6, Python 3.11+, and
[`uv`](https://docs.astral.sh/uv/). Portless is required only for the HTTPS
development hostname; the direct localhost commands below work without it.
From the repository root:
```bash
bun install --frozen-lockfile
cp apps/sdk-playground/.env.example apps/sdk-playground/.env.local
# Required:
# SUPERMEMORY_API_KEY=...
# OPENAI_API_KEY=...
```
The playground scripts build `@supermemory/tools` first and
`@supermemory/ai-sdk` second before starting, type-checking, or building the
Next.js app. Development mode also watches both workspace packages.
## Run
```bash
bun run --cwd apps/sdk-playground dev
```
Opens:
- **Chat UI** — https://sdk.dev.supermemory.ai via Portless
- **Next.js server** — http://127.0.0.1:3005
- **Python server** — http://127.0.0.1:8792
To run without Portless, use two terminals:
```bash
bun run --cwd apps/sdk-playground dev:next
bun run --cwd apps/sdk-playground dev:python
```
For a production-mode local smoke check, build first and then start. `start`
runs both the built Next.js app and the Python server, and remains intended for
local use only.
```bash
bun run --cwd apps/sdk-playground build
bun run --cwd apps/sdk-playground start
```
Try:
- "Remember that I prefer oat milk in coffee"
- "What do you know about my drink preferences?"
- "Forget that I like tea" (tools mode)
## Env
| Variable | Required |
|----------|----------|
| `SUPERMEMORY_API_KEY` | yes |
| `OPENAI_API_KEY` | yes |
| `SUPERMEMORY_BASE_URL` | optional |
| `MODEL_NAME` | optional (default `gpt-4o-mini`) |
| `SDK_PLAYGROUND_PYTHON_URL` | optional (default `http://127.0.0.1:8792`) |
| `SDK_PLAYGROUND_PYTHON_PORT` | optional (default `8792`) |
| `SDK_PLAYGROUND_ALLOW_ENV_KEYS` | optional; set `true` only when a trusted non-local hostname must use server env keys |
Server environment keys are exposed to the playground routes only on loopback
hosts and `sdk.dev.supermemory.ai` by default. Browser-provided keys remain
request-scoped and are never copied into process-global environment variables.

View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
transpilePackages: ["@supermemory/tools", "@supermemory/ai-sdk"],
}
export default nextConfig

View file

@ -0,0 +1,39 @@
{
"name": "sdk-playground",
"version": "0.1.0",
"private": true,
"portless": { "name": "sdk.dev.supermemory", "script": "dev:app" },
"scripts": {
"dev": "portless",
"dev:app": "bun run build:dependencies && concurrently -k -n tools,ai-sdk,next,py -c yellow,magenta,blue,green \"bun run --cwd ../../packages/tools dev\" \"bun run --cwd ../../packages/ai-sdk dev\" \"next dev --hostname 127.0.0.1 --port ${PORT:-3005}\" \"bun run dev:python\"",
"dev:next": "bun run build:dependencies && next dev --hostname 127.0.0.1 --port ${PORT:-3005}",
"dev:python": "cd python && uv run server.py",
"build:dependencies": "bun run --cwd ../../packages/tools build && bun run --cwd ../../packages/ai-sdk build",
"build:app": "next build",
"build": "bun run build:dependencies && bun run build:app",
"start": "concurrently -k -n next,py -c blue,green \"next start --hostname 127.0.0.1 --port ${PORT:-3005}\" \"bun run dev:python\"",
"typegen": "next typegen",
"check-types:app": "bun run typegen && tsc --noEmit --incremental false",
"check-types": "bun run build:dependencies && bun run check-types:app"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.22",
"@supermemory/ai-sdk": "workspace:*",
"@supermemory/tools": "workspace:*",
"ai": "^5.0.113",
"next": "16.0.7",
"openai": "^4.104.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"supermemory": "^4.25.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.1.2",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View file

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
}
export default config

View file

@ -0,0 +1,16 @@
[project]
name = "sdk-playground-python"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115.0",
"httpx>=0.28.0",
"uvicorn[standard]>=0.32.0",
"python-dotenv>=1.0.1",
"supermemory>=3.50.0",
"openai>=1.102.0",
"supermemory-openai-sdk[async]",
]
[tool.uv.sources]
supermemory-openai-sdk = { path = "../../../packages/openai-sdk-python", editable = true }

View file

@ -0,0 +1,841 @@
"""HTTP server for Python SDK chat integrations in the playground."""
import asyncio
import hashlib
import json
import os
import re
import time
from pathlib import Path
from typing import Annotated, Any, Literal, Optional
from urllib.parse import urlparse
from dotenv import load_dotenv
from fastapi import FastAPI, Header, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, SecretStr, model_validator
from starlette.middleware.trustedhost import TrustedHostMiddleware
_root = Path(__file__).resolve().parent
load_dotenv(_root / ".env")
load_dotenv(_root.parent / ".env.local")
load_dotenv(_root.parent / ".env")
DEFAULT_SUPERMEMORY_BASE_URL = "https://api.supermemory.ai"
HTTP_TIMEOUT_SECONDS = 60.0
CHAT_TIMEOUT_SECONDS = 115.0
CONTEXT_DEBUG_TIMEOUT_SECONDS = 10.0
DIRECT_SAVE_TIMEOUT_SECONDS = 10.0
MAX_OUTPUT_TOKENS = 2_048
MAX_MESSAGE_LENGTH = 20_000
MAX_MESSAGES = 64
MAX_TOTAL_MESSAGE_LENGTH = 100_000
MAX_API_KEY_LENGTH = 1_024
MAX_CONTAINER_TAG_LENGTH = 100
MAX_CONVERSATION_ID_LENGTH = 242
CONTAINER_TAG_PATTERN = r"^[a-zA-Z0-9_:-]+$"
TOOLS_SYSTEM_PROMPT = """You are a helpful assistant with Supermemory long-term memory.
You have tools to manage memory. Use them proactively:
- search_memories: hybrid recall search before answering whenever user-specific context could help (do not wait to be asked)
- get_profile: broad static/dynamic user context at conversation start or when you need a wide overview
- add_memory: store a new generalizable fact
- document_list / document_add / document_delete: manage source documents
- memory_forget: soft-delete one profile fact (not whole documents)
Before answering questions about the user, their preferences, or past context, search memories or get profile first. When the user asks you to remember something, use add_memory."""
app = FastAPI(title="SDK Playground Python Chat")
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["127.0.0.1", "localhost"],
)
class ChatMessage(BaseModel):
role: Literal["user", "assistant", "system"]
content: str = Field(max_length=MAX_MESSAGE_LENGTH)
class MiddlewareConfig(BaseModel):
addMemory: Literal["always", "never"] = "always"
verbose: bool = False
class PlaygroundInputError(ValueError):
"""A request value is missing after transport-level validation."""
class SupermemoryApiKeys(BaseModel):
supermemoryApiKey: SecretStr = Field(max_length=MAX_API_KEY_LENGTH)
class ApiKeys(SupermemoryApiKeys):
openaiApiKey: SecretStr = Field(max_length=MAX_API_KEY_LENGTH)
class ChatRequest(BaseModel):
sdkId: Literal[
"py-openai-middleware",
"py-openai-tools",
"py-supermemory-direct",
]
messages: list[ChatMessage] = Field(min_length=1, max_length=MAX_MESSAGES)
containerTag: str = Field(
default="sdk-playground",
min_length=1,
max_length=MAX_CONTAINER_TAG_LENGTH,
pattern=CONTAINER_TAG_PATTERN,
)
conversationId: str = Field(
min_length=1,
max_length=MAX_CONVERSATION_ID_LENGTH,
)
memoryMode: Optional[Literal["profile", "query", "full"]] = "full"
middlewareConfig: Optional[MiddlewareConfig] = None
apiKeys: Optional[ApiKeys] = None
@model_validator(mode="after")
def require_user_message(self) -> "ChatRequest":
if not any(
message.role == "user" and message.content.strip()
for message in self.messages
):
raise ValueError("messages must include a non-empty user message")
if (
sum(len(message.content) for message in self.messages)
> MAX_TOTAL_MESSAGE_LENGTH
):
raise ValueError(
f"total message content cannot exceed {MAX_TOTAL_MESSAGE_LENGTH} characters"
)
return self
class ContextRequest(BaseModel):
containerTag: str = Field(
default="sdk-playground",
min_length=1,
max_length=MAX_CONTAINER_TAG_LENGTH,
pattern=CONTAINER_TAG_PATTERN,
)
query: Optional[str] = Field(default=None, max_length=MAX_MESSAGE_LENGTH)
apiKeys: Optional[SupermemoryApiKeys] = None
def model_name() -> str:
return os.getenv("MODEL_NAME", "gpt-4o-mini")
def supplied_secret(value: Optional[SecretStr], label: str) -> str:
secret = value.get_secret_value().strip() if value else ""
if not secret:
raise PlaygroundInputError(f"{label} must be supplied with the request")
return secret
def resolve_supermemory_key(api_keys: Optional[SupermemoryApiKeys]) -> str:
return supplied_secret(
api_keys.supermemoryApiKey if api_keys else None,
"Supermemory API key",
)
def resolve_chat_keys(api_keys: Optional[ApiKeys]) -> tuple[str, str]:
return (
resolve_supermemory_key(api_keys),
supplied_secret(api_keys.openaiApiKey if api_keys else None, "OpenAI API key"),
)
def supermemory_base_url() -> str:
configured = os.getenv("SUPERMEMORY_BASE_URL", "").strip()
base_url = (configured or DEFAULT_SUPERMEMORY_BASE_URL).rstrip("/")
parsed = urlparse(base_url)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise RuntimeError("SUPERMEMORY_BASE_URL must be an absolute HTTP(S) URL")
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise RuntimeError(
"SUPERMEMORY_BASE_URL cannot contain credentials, a query, or a fragment"
)
return base_url
def public_error(error: Exception, *secrets: str) -> str:
message = str(error)
for secret in secrets:
if secret:
message = message.replace(secret, "[redacted]")
return message[:1_000]
async def chat_openai_middleware(
messages: list[ChatMessage],
container_tag: str,
conversation_id: str,
memory_mode: str,
middleware_config: MiddlewareConfig,
sm_key: str,
oai_key: str,
) -> str:
from openai import AsyncOpenAI
from supermemory_openai import OpenAIMiddlewareOptions, with_supermemory
client = with_supermemory(
AsyncOpenAI(
api_key=oai_key,
timeout=HTTP_TIMEOUT_SECONDS,
max_retries=1,
),
OpenAIMiddlewareOptions(
container_tag=container_tag,
custom_id=conversation_id,
mode=memory_mode,
add_memory=middleware_config.addMemory,
verbose=middleware_config.verbose,
api_key=sm_key,
base_url=supermemory_base_url(),
),
)
openai_messages = [m.model_dump() for m in messages]
if not any(m.role == "system" for m in messages):
openai_messages.insert(
0,
{
"role": "system",
"content": (
"You are a helpful assistant with long-term memory about the user."
),
},
)
response = await client.chat.completions.create(
model=model_name(),
messages=openai_messages,
max_completion_tokens=MAX_OUTPUT_TOKENS,
)
return response.choices[0].message.content or ""
async def chat_openai_tools(
messages: list[ChatMessage],
container_tag: str,
sm_key: str,
oai_key: str,
) -> tuple[str, list[dict[str, Any]]]:
from openai import AsyncOpenAI
from supermemory_openai import SupermemoryTools, execute_memory_tool_calls
openai_client = AsyncOpenAI(
api_key=oai_key,
timeout=HTTP_TIMEOUT_SECONDS,
max_retries=1,
)
config: dict[str, Any] = {
"base_url": supermemory_base_url(),
"container_tags": [container_tag],
}
tools = SupermemoryTools(sm_key, config)
tool_defs = tools.get_tool_definitions()
trace: list[dict[str, Any]] = []
convo: list[dict[str, Any]] = [
{"role": "system", "content": TOOLS_SYSTEM_PROMPT},
*[m.model_dump() for m in messages if m.role != "system"],
]
for step in range(8):
response = await openai_client.chat.completions.create(
model=model_name(),
messages=convo,
tools=tool_defs,
max_completion_tokens=MAX_OUTPUT_TOKENS,
)
message = response.choices[0].message
convo.append(message.model_dump())
if message.tool_calls:
tool_messages = await execute_memory_tool_calls(
sm_key,
message.tool_calls,
config,
)
for i, call in enumerate(message.tool_calls):
raw = tool_messages[i]["content"]
try:
parsed = json.loads(raw)
except json.JSONDecodeError:
parsed = raw
trace.append(
{
"step": step + 1,
"toolName": call.function.name,
"args": json.loads(call.function.arguments),
"result": parsed,
}
)
convo.extend(tool_messages)
continue
return message.content or "", trace
raise RuntimeError("Tool loop exceeded max steps")
def object_field(value: Any, name: str, default: Any = None) -> Any:
if isinstance(value, dict):
return value.get(name, default)
return getattr(value, name, default)
def list_field(value: Any, name: str) -> list[Any]:
result = object_field(value, name, [])
return result if isinstance(result, list) else []
def extract_profile_context(profile_response: Any) -> dict[str, list[Any]]:
profile = object_field(profile_response, "profile", {}) or {}
search_results = object_field(profile_response, "search_results", None)
if search_results is None and isinstance(profile_response, dict):
search_results = profile_response.get("searchResults")
if isinstance(search_results, list):
search_list = search_results
else:
search_list = list_field(search_results, "results")
return {
"static": list_field(profile, "static"),
"dynamic": list_field(profile, "dynamic"),
"searchResults": search_list,
}
def display_context_item(item: Any) -> str:
if hasattr(item, "model_dump"):
return json.dumps(item.model_dump(mode="json"), ensure_ascii=False)
if isinstance(item, dict):
return json.dumps(item, ensure_ascii=False)
return str(item)
def direct_conversation_custom_id(conversation_id: str) -> str:
readable = re.sub(r"[^A-Za-z0-9._-]+", "-", conversation_id).strip("-._")
readable = readable[:40] or "session"
digest = hashlib.sha256(conversation_id.encode("utf-8")).hexdigest()[:12]
return f"sdk-playground-direct-{readable}-{digest}"
def conversation_transcript(messages: list[ChatMessage], assistant_text: str) -> str:
transcript = [
f"{message.role.capitalize()}: {message.content}"
for message in messages
if message.role != "system"
]
transcript.append(f"Assistant: {assistant_text or '(empty response)'}")
return "\n\n".join(transcript)
async def fetch_profile_context(
container_tag: str,
sm_key: str,
query: Optional[str] = None,
) -> dict[str, list[Any]]:
from supermemory import AsyncSupermemory
client = AsyncSupermemory(
api_key=sm_key,
base_url=supermemory_base_url(),
timeout=HTTP_TIMEOUT_SECONDS,
)
profile_response = await client.profile(
container_tag=container_tag,
**({"q": query} if query else {}),
)
return extract_profile_context(profile_response)
async def chat_supermemory_direct(
messages: list[ChatMessage],
container_tag: str,
conversation_id: str,
sm_key: str,
oai_key: str,
) -> tuple[str, str, dict[str, list[Any]]]:
"""Manual pattern: profile() for context, then OpenAI, then add() conversation."""
from openai import AsyncOpenAI
from supermemory import AsyncSupermemory
sm_client = AsyncSupermemory(
api_key=sm_key,
base_url=supermemory_base_url(),
timeout=HTTP_TIMEOUT_SECONDS,
)
openai_client = AsyncOpenAI(
api_key=oai_key,
timeout=HTTP_TIMEOUT_SECONDS,
max_retries=1,
)
user_messages = [m for m in messages if m.role == "user"]
last_user = user_messages[-1].content if user_messages else ""
profile_response = await sm_client.profile(
container_tag=container_tag,
**({"q": last_user} if last_user else {}),
)
profile_context = extract_profile_context(profile_response)
context = "\n".join(
(
"Profile static: "
+ ", ".join(map(display_context_item, profile_context["static"])),
"Profile dynamic: "
+ ", ".join(map(display_context_item, profile_context["dynamic"])),
"Relevant search results: "
+ ", ".join(map(display_context_item, profile_context["searchResults"])),
)
)
openai_messages: list[dict[str, str]] = [
{
"role": "system",
"content": f"You are a helpful assistant. User context:\n{context}",
},
*[m.model_dump() for m in messages if m.role != "system"],
]
response = await openai_client.chat.completions.create(
model=model_name(),
messages=openai_messages,
max_completion_tokens=MAX_OUTPUT_TOKENS,
)
assistant_text = response.choices[0].message.content or ""
custom_id = direct_conversation_custom_id(conversation_id)
return assistant_text, custom_id, profile_context
async def save_direct_conversation(
messages: list[ChatMessage],
assistant_text: str,
container_tag: str,
custom_id: str,
sm_key: str,
) -> dict[str, Any]:
from supermemory import AsyncSupermemory
try:
client = AsyncSupermemory(
api_key=sm_key,
base_url=supermemory_base_url(),
timeout=DIRECT_SAVE_TIMEOUT_SECONDS,
)
async with asyncio.timeout(DIRECT_SAVE_TIMEOUT_SECONDS):
response = await client.add(
content=conversation_transcript(messages, assistant_text),
container_tag=container_tag,
custom_id=custom_id,
)
return {
"type": "conversation_save_accepted",
"label": "Full conversation accepted for processing",
"detail": {
"nonFatal": True,
"containerTag": container_tag,
"customId": custom_id,
"documentId": object_field(response, "id"),
"status": object_field(response, "status"),
},
}
except Exception as error:
return {
"type": "conversation_save_failed",
"label": "Conversation save unavailable",
"detail": {
"nonFatal": True,
"containerTag": container_tag,
"customId": custom_id,
"error": public_error(error, sm_key),
},
}
async def fetch_container_context(
container_tag: str,
sm_key: str,
query: Optional[str] = None,
) -> dict[str, Any]:
if not sm_key:
raise RuntimeError("Supermemory API key must be supplied")
profile_context = await fetch_profile_context(container_tag, sm_key, query)
base_url = supermemory_base_url()
import httpx
async with httpx.AsyncClient(
timeout=HTTP_TIMEOUT_SECONDS,
follow_redirects=False,
) as http:
docs_response = await http.post(
f"{base_url}/v3/documents/documents",
headers={
"Authorization": f"Bearer {sm_key}",
"Content-Type": "application/json",
},
json={
"containerTags": [container_tag],
"limit": 25,
"sort": "createdAt",
"order": "desc",
},
)
docs_response.raise_for_status()
docs = docs_response.json()
raw_documents = docs.get("documents", []) if isinstance(docs, dict) else []
documents = []
for doc in raw_documents:
record = doc if isinstance(doc, dict) else getattr(doc, "__dict__", {})
memory_entries = (
record.get("memoryEntries") or record.get("memory_entries") or []
)
if not memory_entries and isinstance(record.get("memories"), list):
nested = record.get("memories") or []
if nested and isinstance(nested[0], dict) and nested[0].get("memory"):
memory_entries = nested
documents.append(
{
"id": record.get("id"),
"title": record.get("title"),
"status": record.get("status"),
"customId": record.get("customId") or record.get("custom_id"),
"createdAt": record.get("createdAt") or record.get("created_at"),
"updatedAt": record.get("updatedAt") or record.get("updated_at"),
"summary": record.get("summary"),
"memoryEntries": memory_entries,
}
)
return {
"containerTag": container_tag,
"query": query,
"profile": profile_context,
"documents": documents,
"pagination": docs.get("pagination") if isinstance(docs, dict) else None,
}
def build_middleware_memory_debug(
container_tag: str,
conversation_id: str,
memory_mode: str,
last_user_message: str,
context: Optional[dict[str, Any]],
context_error: Optional[str],
middleware_config: MiddlewareConfig,
) -> list[dict[str, Any]]:
debug: list[dict[str, Any]] = []
if context is None:
debug.append(
{
"type": "context_debug_unavailable",
"label": "Post-response context snapshot unavailable",
"detail": {"error": context_error or "Unknown context error"},
}
)
else:
profile = context["profile"]
preview_lines = [
f"[memory mode: {memory_mode}]",
"[post-response snapshot; not the exact middleware prompt]",
]
if context.get("query"):
preview_lines.append(f"[query: {context['query']}]")
selected_sections: list[tuple[str, list[Any]]] = []
if memory_mode in ("profile", "full"):
selected_sections.extend(
(
("Static", profile.get("static", [])),
("Dynamic", profile.get("dynamic", [])),
)
)
if memory_mode in ("query", "full"):
selected_sections.append(
("Search results", profile.get("searchResults", []))
)
for label, items in selected_sections:
if items:
preview_lines.append(f"{label}:")
for item in items[:8]:
preview_lines.append(f"- {display_context_item(item)}")
debug.extend(
(
{
"type": "profile_fetch",
"label": "Post-response profile snapshot",
"detail": {
"endpoint": "POST /v4/profile",
"containerTag": container_tag,
"customId": conversation_id,
"memoryMode": memory_mode,
"query": context.get("query"),
"staticCount": len(profile.get("static", [])),
"dynamicCount": len(profile.get("dynamic", [])),
"searchResultCount": len(profile.get("searchResults", [])),
},
},
{
"type": "context_preview",
"label": "Post-response context preview",
"preview": "\n".join(preview_lines),
},
)
)
save_detail = {
"containerTag": container_tag,
"customId": f"conversation:{conversation_id}",
"addMemory": middleware_config.addMemory,
"verbose": middleware_config.verbose,
}
if middleware_config.addMemory == "always" and last_user_message.strip():
debug.append(
{
"type": "conversation_save_queued",
"label": "Conversation save queued by middleware",
"detail": save_detail,
}
)
else:
debug.append(
{
"type": "conversation_save_skipped",
"label": "Conversation save disabled",
"detail": save_detail,
}
)
return debug
async def fetch_context_for_debug(
container_tag: str,
query: Optional[str],
sm_key: str,
) -> tuple[Optional[dict[str, Any]], Optional[str]]:
try:
async with asyncio.timeout(CONTEXT_DEBUG_TIMEOUT_SECONDS):
profile = await fetch_profile_context(container_tag, sm_key, query)
return (
{
"containerTag": container_tag,
"query": query,
"profile": profile,
},
None,
)
except Exception as error:
return None, public_error(error, sm_key)
@app.get("/context")
async def context_get(
containerTag: Annotated[
str,
Query(
min_length=1,
max_length=MAX_CONTAINER_TAG_LENGTH,
pattern=CONTAINER_TAG_PATTERN,
),
] = "sdk-playground",
query: Annotated[Optional[str], Query(max_length=MAX_MESSAGE_LENGTH)] = None,
x_supermemory_api_key: Annotated[
Optional[str],
Header(alias="X-Supermemory-API-Key"),
] = None,
):
sm_key = ""
try:
sm_key = supplied_secret(
SecretStr(x_supermemory_api_key) if x_supermemory_api_key else None,
"X-Supermemory-API-Key header",
)
async with asyncio.timeout(HTTP_TIMEOUT_SECONDS):
ctx = await fetch_container_context(containerTag, sm_key, query)
return {"ok": True, "context": ctx}
except Exception as error:
return JSONResponse(
status_code=(
504
if isinstance(error, TimeoutError)
else 400 if isinstance(error, PlaygroundInputError) else 500
),
content={"ok": False, "error": public_error(error, sm_key)},
)
@app.post("/context")
async def context_post(req: ContextRequest):
sm_key = ""
try:
sm_key = resolve_supermemory_key(req.apiKeys)
async with asyncio.timeout(HTTP_TIMEOUT_SECONDS):
ctx = await fetch_container_context(req.containerTag, sm_key, req.query)
return {"ok": True, "context": ctx}
except Exception as error:
return JSONResponse(
status_code=(
504
if isinstance(error, TimeoutError)
else 400 if isinstance(error, PlaygroundInputError) else 500
),
content={"ok": False, "error": public_error(error, sm_key)},
)
@app.get("/health")
async def health():
return {
"ok": True,
"playground": "sdk-playground",
"requiresRequestKeys": True,
"model": model_name(),
"sdks": [
"py-openai-middleware",
"py-openai-tools",
"py-supermemory-direct",
],
}
@app.post("/chat")
async def chat(req: ChatRequest):
started = time.time()
sm_key = ""
oai_key = ""
try:
sm_key, oai_key = resolve_chat_keys(req.apiKeys)
tool_trace: list[dict[str, Any]] = []
memory_debug: list[dict[str, Any]] = []
middleware_debug: Optional[tuple[MiddlewareConfig, str, Optional[str]]] = None
direct_debug: Optional[tuple[str, dict[str, list[Any]], str]] = None
async with asyncio.timeout(CHAT_TIMEOUT_SECONDS):
if req.sdkId == "py-openai-middleware":
middleware_config = req.middlewareConfig or MiddlewareConfig()
text = await chat_openai_middleware(
req.messages,
req.containerTag,
req.conversationId,
req.memoryMode or "full",
middleware_config,
sm_key,
oai_key,
)
last_user = next(
(m.content for m in reversed(req.messages) if m.role == "user"),
"",
)
query = last_user if req.memoryMode != "profile" else None
middleware_debug = (middleware_config, last_user, query)
elif req.sdkId == "py-openai-tools":
text, tool_trace = await chat_openai_tools(
req.messages, req.containerTag, sm_key, oai_key
)
elif req.sdkId == "py-supermemory-direct":
text, custom_id, profile_context = await chat_supermemory_direct(
req.messages,
req.containerTag,
req.conversationId,
sm_key,
oai_key,
)
last_user = next(
(m.content for m in reversed(req.messages) if m.role == "user"),
"",
)
direct_debug = (custom_id, profile_context, last_user)
else:
raise RuntimeError(f"Unsupported Python SDK: {req.sdkId}")
if middleware_debug is not None:
middleware_config, last_user, query = middleware_debug
ctx, context_error = await fetch_context_for_debug(
req.containerTag,
query,
sm_key,
)
memory_debug = build_middleware_memory_debug(
req.containerTag,
req.conversationId,
req.memoryMode or "full",
last_user,
ctx,
context_error,
middleware_config,
)
elif direct_debug is not None:
custom_id, profile_context, last_user = direct_debug
save_debug = await save_direct_conversation(
req.messages,
text,
req.containerTag,
custom_id,
sm_key,
)
memory_debug = [
{
"type": "manual_profile",
"label": "Profile context used for this response",
"detail": {
"containerTag": req.containerTag,
"query": last_user,
"staticCount": len(profile_context["static"]),
"dynamicCount": len(profile_context["dynamic"]),
"searchResultCount": len(profile_context["searchResults"]),
},
},
save_debug,
]
return {
"ok": True,
"sdkId": req.sdkId,
"message": {"role": "assistant", "content": text},
"toolTrace": tool_trace,
"memoryDebug": memory_debug,
"durationMs": int((time.time() - started) * 1000),
}
except TimeoutError:
return JSONResponse(
status_code=504,
content={
"ok": False,
"sdkId": req.sdkId,
"error": f"Python chat timed out after {int(CHAT_TIMEOUT_SECONDS)} seconds",
"durationMs": int((time.time() - started) * 1000),
},
)
except Exception as error:
return JSONResponse(
status_code=400 if isinstance(error, PlaygroundInputError) else 500,
content={
"ok": False,
"sdkId": req.sdkId,
"error": public_error(error, sm_key, oai_key),
"durationMs": int((time.time() - started) * 1000),
},
)
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("SDK_PLAYGROUND_PYTHON_PORT", "8792"))
uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")

1557
apps/sdk-playground/python/uv.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,167 @@
import { NextResponse } from "next/server"
import { CHAT_SDK_REGISTRY, PYTHON_SERVER_URL } from "@/lib/sdk-registry"
import {
resolveApiKeys,
resolveOpenAiApiKey,
resolveSupermemoryApiKey,
} from "@/lib/api-keys"
import {
PlaygroundChatTimeoutError,
runTypeScriptChat,
} from "@/lib/chat-handlers"
import {
PlaygroundRequestError,
assertTrustedBrowserRequest,
mayUseEnvironmentKeys,
parseApiKeys,
parseContainerTag,
parseConversationId,
parseIdentifier,
parseMemoryMode,
parseMessages,
parseMiddlewareConfig,
readJsonObject,
} from "@/lib/request-validation"
const PYTHON_HEALTH_TIMEOUT_MS = 2_000
// Python reserves 115s for the model/tool path and up to 10s for nonfatal debug.
const PYTHON_CHAT_TIMEOUT_MS = 130_000
export async function GET(request: Request) {
let pythonOk = false
try {
const res = await fetch(`${PYTHON_SERVER_URL}/health`, {
cache: "no-store",
signal: AbortSignal.timeout(PYTHON_HEALTH_TIMEOUT_MS),
})
if (res.ok) {
const data = await res.json()
pythonOk = data.playground === "sdk-playground"
}
} catch {
pythonOk = false
}
const allowEnvironment = mayUseEnvironmentKeys(request)
return NextResponse.json({
sdks: CHAT_SDK_REGISTRY,
hasSupermemoryKey: Boolean(
resolveSupermemoryApiKey(null, { allowEnvironment }),
),
hasOpenAiKey: Boolean(resolveOpenAiApiKey(null, { allowEnvironment })),
pythonUrl: PYTHON_SERVER_URL,
model: process.env.MODEL_NAME ?? "gpt-4o-mini",
pythonOk,
})
}
export async function POST(req: Request) {
const started = Date.now()
try {
assertTrustedBrowserRequest(req)
const body = await readJsonObject(req)
const sdkId = parseIdentifier(body.sdkId, "sdkId")
const messages = parseMessages(body.messages)
const containerTag = parseContainerTag(body.containerTag)
const conversationId = parseConversationId(body.conversationId)
const memoryMode = parseMemoryMode(body.memoryMode)
const middlewareConfig = parseMiddlewareConfig(body.middlewareConfig)
const apiKeys = resolveApiKeys(parseApiKeys(body.apiKeys), {
allowEnvironment: mayUseEnvironmentKeys(req),
})
if (!apiKeys) {
return NextResponse.json(
{
ok: false,
error:
"Supermemory and OpenAI API keys are required — enter them in the dashboard or set env vars.",
},
{ status: 400 },
)
}
const sdk = CHAT_SDK_REGISTRY.find((s) => s.id === sdkId)
if (!sdk?.available) {
return NextResponse.json(
{ ok: false, error: `SDK not available: ${sdkId}` },
{ status: 400 },
)
}
if (sdk.language === "python") {
const res = await fetch(`${PYTHON_SERVER_URL}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sdkId,
messages,
containerTag,
conversationId,
memoryMode,
middlewareConfig,
apiKeys,
}),
signal: AbortSignal.timeout(PYTHON_CHAT_TIMEOUT_MS),
})
const data = await res.json()
if (!res.ok && !data.error) {
return NextResponse.json(
{
ok: false,
error: `Python server error (${res.status})`,
durationMs: Date.now() - started,
},
{ status: res.status },
)
}
return NextResponse.json(
{
...data,
durationMs: Date.now() - started,
},
{ status: res.ok ? 200 : res.status },
)
}
const result = await runTypeScriptChat(
{
sdkId,
messages,
containerTag,
conversationId,
memoryMode,
middlewareConfig,
apiKeys,
containerTags: [containerTag],
},
apiKeys,
)
return NextResponse.json({
ok: true,
sdkId,
message: { role: "assistant", content: result.text },
toolTrace: result.toolTrace,
memoryDebug: result.memoryDebug,
durationMs: Date.now() - started,
})
} catch (error) {
const status =
error instanceof PlaygroundRequestError
? error.status
: error instanceof PlaygroundChatTimeoutError ||
(error instanceof Error && error.name === "TimeoutError")
? 504
: 500
return NextResponse.json(
{
ok: false,
durationMs: Date.now() - started,
error: error instanceof Error ? error.message : String(error),
},
{ status },
)
}
}

View file

@ -0,0 +1,90 @@
import { NextResponse } from "next/server"
import { resolveSupermemoryApiKey } from "@/lib/api-keys"
import { fetchContainerContext } from "@/lib/context-api"
import {
PlaygroundRequestError,
assertTrustedBrowserRequest,
mayUseEnvironmentKeys,
parseApiKeys,
parseContainerTag,
parseOptionalText,
readJsonObject,
} from "@/lib/request-validation"
export async function GET(req: Request) {
try {
assertTrustedBrowserRequest(req)
const { searchParams } = new URL(req.url)
const containerTag = parseContainerTag(searchParams.get("containerTag"))
const query = parseOptionalText(searchParams.get("query"), "query")
const supermemoryApiKey = resolveSupermemoryApiKey(null, {
allowEnvironment: mayUseEnvironmentKeys(req),
})
if (!supermemoryApiKey) {
return NextResponse.json(
{
ok: false,
error:
"Supermemory API key is required — enter it in the dashboard or set a local env var.",
},
{ status: 400 },
)
}
const context = await fetchContainerContext(
containerTag,
query,
supermemoryApiKey,
)
return NextResponse.json({ ok: true, context })
} catch (error) {
const status = error instanceof PlaygroundRequestError ? error.status : 500
return NextResponse.json(
{
ok: false,
error: error instanceof Error ? error.message : String(error),
},
{ status },
)
}
}
export async function POST(req: Request) {
try {
assertTrustedBrowserRequest(req)
const body = await readJsonObject(req)
const containerTag = parseContainerTag(body.containerTag)
const query = parseOptionalText(body.query, "query")
const supermemoryApiKey = resolveSupermemoryApiKey(
parseApiKeys(body.apiKeys),
{ allowEnvironment: mayUseEnvironmentKeys(req) },
)
if (!supermemoryApiKey) {
return NextResponse.json(
{
ok: false,
error: "Supermemory API key is required — enter it in the dashboard.",
},
{ status: 400 },
)
}
const context = await fetchContainerContext(
containerTag,
query,
supermemoryApiKey,
)
return NextResponse.json({ ok: true, context })
} catch (error) {
const status = error instanceof PlaygroundRequestError ? error.status : 500
return NextResponse.json(
{
ok: false,
error: error instanceof Error ? error.message : String(error),
},
{ status },
)
}
}

View file

@ -0,0 +1,18 @@
@import "tailwindcss";
:root {
color-scheme: dark;
}
body {
font-family:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
}
textarea,
select,
input,
button {
font: inherit;
}

View file

@ -0,0 +1,19 @@
import type { Metadata } from "next"
import "./globals.css"
export const metadata: Metadata = {
title: "Supermemory SDK Playground",
description: "Switch and test Supermemory SDKs across languages",
}
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body className="min-h-screen bg-zinc-950 text-zinc-100 antialiased">
{children}
</body>
</html>
)
}

View file

@ -0,0 +1,623 @@
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { ApiKeysPanel } from "@/components/ApiKeysPanel"
import { ContextPanel } from "@/components/ContextPanel"
import { ToolsReferencePanel } from "@/components/ToolsReferencePanel"
import type { MemoryDebugEntry } from "@/lib/context-api"
import {
DEFAULT_MIDDLEWARE_CONFIG,
type MiddlewareRuntimeConfig,
} from "@/lib/middleware-config"
import {
CHAT_SDK_REGISTRY,
type ChatSdkDefinition,
type ToolTraceEntry,
} from "@/lib/sdk-registry"
type UserOrAssistantMessage = {
kind: "user" | "assistant"
content: string
}
type ToolMessage = {
kind: "tool"
entry: ToolTraceEntry
}
type DebugMessage = {
kind: "debug"
entry: MemoryDebugEntry
}
type DisplayMessage = UserOrAssistantMessage | ToolMessage | DebugMessage
export default function AgentPlaygroundPage() {
const [sdks, setSdks] = useState(CHAT_SDK_REGISTRY)
const [hasSupermemoryKey, setHasSupermemoryKey] = useState(false)
const [hasOpenAiKey, setHasOpenAiKey] = useState(false)
const [pythonOk, setPythonOk] = useState(false)
const [model, setModel] = useState("gpt-4o-mini")
const [pythonUrl, setPythonUrl] = useState("http://127.0.0.1:8792")
const [supermemoryApiKey, setSupermemoryApiKey] = useState("")
const [openaiApiKey, setOpenaiApiKey] = useState("")
const apiKeys = useMemo(
() => ({ supermemoryApiKey, openaiApiKey }),
[supermemoryApiKey, openaiApiKey],
)
const supermemoryKeyReady =
supermemoryApiKey.trim().length > 0 || hasSupermemoryKey
const openAiKeyReady = openaiApiKey.trim().length > 0 || hasOpenAiKey
const keysReady = supermemoryKeyReady && openAiKeyReady
const [sdkId, setSdkId] = useState("ts-ai-sdk-middleware")
const [containerTag, setContainerTag] = useState("sdk-playground")
const [memoryMode, setMemoryMode] = useState<"profile" | "query" | "full">(
"full",
)
const [conversationId, setConversationId] = useState("")
const [middlewareConfig, setMiddlewareConfig] =
useState<MiddlewareRuntimeConfig>(DEFAULT_MIDDLEWARE_CONFIG)
const [messages, setMessages] = useState<DisplayMessage[]>([])
const [input, setInput] = useState("")
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [contextRefreshKey, setContextRefreshKey] = useState(0)
const [leftPanel, setLeftPanel] = useState<"sdks" | "tools">("sdks")
const lastUserMessage = useMemo(() => {
const users = messages.filter(
(m): m is UserOrAssistantMessage => m.kind === "user",
)
return users.at(-1)?.content ?? ""
}, [messages])
const selectedSdk = useMemo(
() => sdks.find((s) => s.id === sdkId),
[sdks, sdkId],
)
useEffect(() => {
setConversationId((current) => current || crypto.randomUUID())
}, [])
const refreshMeta = useCallback(async () => {
try {
const res = await fetch("/api/chat")
const data = await res.json()
if (data.sdks) setSdks(data.sdks)
setHasSupermemoryKey(Boolean(data.hasSupermemoryKey))
setHasOpenAiKey(Boolean(data.hasOpenAiKey))
setPythonOk(Boolean(data.pythonOk))
if (data.pythonUrl) setPythonUrl(data.pythonUrl)
if (data.model) setModel(data.model)
} catch {
/* ignore */
}
}, [])
useEffect(() => {
refreshMeta()
}, [refreshMeta])
const send = async () => {
if (!input.trim() || loading || !selectedSdk?.available || !keysReady)
return
const activeConversationId = conversationId.trim() || crypto.randomUUID()
if (!conversationId.trim()) setConversationId(activeConversationId)
const userMessage: UserOrAssistantMessage = {
kind: "user",
content: input.trim(),
}
const chatHistory = messages
.filter(
(m): m is UserOrAssistantMessage =>
m.kind === "user" || m.kind === "assistant",
)
.map((m) => ({ role: m.kind, content: m.content }))
const nextMessages = [...messages, userMessage]
setMessages(nextMessages)
setInput("")
setLoading(true)
setError(null)
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sdkId,
messages: [
...chatHistory,
{ role: "user", content: userMessage.content },
],
containerTag,
conversationId: activeConversationId,
memoryMode:
selectedSdk.mode === "middleware" ? memoryMode : undefined,
middlewareConfig:
selectedSdk.mode === "middleware" ? middlewareConfig : undefined,
apiKeys,
}),
})
const data = await res.json()
if (!data.ok) {
throw new Error(data.error ?? "Chat failed")
}
const content = data.message?.content ?? ""
const toolTrace = (data.toolTrace ?? []) as ToolTraceEntry[]
const memoryDebug = (data.memoryDebug ?? []) as MemoryDebugEntry[]
setMessages((prev) => [
...prev,
...memoryDebug.map((entry) => ({ kind: "debug" as const, entry })),
...toolTrace.map((entry) => ({ kind: "tool" as const, entry })),
{ kind: "assistant", content },
])
setContextRefreshKey((k) => k + 1)
} catch (err) {
setError(err instanceof Error ? err.message : String(err))
} finally {
setLoading(false)
}
}
const tsSdks = sdks.filter((s) => s.language === "typescript")
const pySdks = sdks.filter((s) => s.language === "python")
return (
<div className="mx-auto flex h-screen max-w-[1400px] flex-col p-4 md:p-5">
<header className="mb-4 shrink-0 space-y-3 border-b border-zinc-800 pb-4">
<div>
<h1 className="text-xl font-semibold tracking-tight">
Supermemory Agent Playground
</h1>
<p className="text-sm text-zinc-400">
Talk to a real agent. Switch the underlying SDK integration in the
sidebar middleware auto-injects memory; tools let the model call
memory operations explicitly.
</p>
</div>
<div className="flex flex-wrap gap-2 text-xs">
<Status ok={supermemoryKeyReady} label="Supermemory key" />
<Status ok={openAiKeyReady} label="OpenAI key" />
<Status
ok={pythonOk}
label={`Python ${pythonUrl.replace("http://", "")}`}
/>
<span className="rounded-full border border-zinc-700 px-3 py-1 text-zinc-400">
model: {model}
</span>
</div>
<ApiKeysPanel
supermemoryApiKey={supermemoryApiKey}
openaiApiKey={openaiApiKey}
hasSupermemoryEnvKey={hasSupermemoryKey}
hasOpenAiEnvKey={hasOpenAiKey}
onSupermemoryChange={setSupermemoryApiKey}
onOpenAiChange={setOpenaiApiKey}
/>
</header>
<div className="flex min-h-0 flex-1 gap-3 lg:gap-4">
<aside className="hidden w-60 shrink-0 min-h-0 lg:flex lg:flex-col">
<div className="mb-2 flex gap-1 shrink-0">
<SidebarTab
active={leftPanel === "sdks"}
onClick={() => setLeftPanel("sdks")}
>
SDKs
</SidebarTab>
<SidebarTab
active={leftPanel === "tools"}
onClick={() => setLeftPanel("tools")}
>
Tools
</SidebarTab>
</div>
<div className="min-h-0 flex-1 overflow-y-auto">
{leftPanel === "sdks" ? (
<div className="space-y-4">
<Section title="TypeScript">
<SdkButtons
sdks={tsSdks}
selected={sdkId}
onSelect={setSdkId}
/>
</Section>
<Section title="Python">
<SdkButtons
sdks={pySdks}
selected={sdkId}
onSelect={setSdkId}
/>
</Section>
</div>
) : (
<ToolsReferencePanel />
)}
</div>
</aside>
<div className="flex min-h-0 flex-1 flex-col gap-3">
<div className="flex gap-1 shrink-0 lg:hidden">
<SidebarTab
active={leftPanel === "sdks"}
onClick={() => setLeftPanel("sdks")}
>
Chat
</SidebarTab>
<SidebarTab
active={leftPanel === "tools"}
onClick={() => setLeftPanel("tools")}
>
Tool reference
</SidebarTab>
</div>
{leftPanel === "tools" && (
<div className="max-h-72 shrink-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/20 p-3 lg:hidden">
<ToolsReferencePanel />
</div>
)}
{leftPanel === "sdks" && (
<div className="flex flex-wrap items-end gap-3 md:hidden">
<label className="flex-1 space-y-1">
<span className="text-xs text-zinc-500">SDK</span>
<select
value={sdkId}
onChange={(e) => setSdkId(e.target.value)}
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-2 text-sm"
>
{sdks.map((s) => (
<option key={s.id} value={s.id} disabled={!s.available}>
{s.label}
</option>
))}
</select>
</label>
</div>
)}
{selectedSdk && (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3 py-2 text-sm">
<div className="font-medium">{selectedSdk.label}</div>
<div className="text-zinc-400">{selectedSdk.description}</div>
<div className="mt-1 text-xs text-zinc-500">
{selectedSdk.package} ·{" "}
<span className="text-zinc-400">
{selectedSdk.mode === "middleware"
? "automatic memory"
: selectedSdk.mode === "tools"
? "explicit tools"
: "manual profile + save"}
</span>
</div>
</div>
)}
<div className="flex flex-wrap gap-3">
<label className="space-y-1">
<span className="text-xs text-zinc-500">Container tag</span>
<input
value={containerTag}
onChange={(e) => setContainerTag(e.target.value)}
maxLength={100}
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm w-40"
/>
</label>
<label className="space-y-1">
<span className="text-xs text-zinc-500">customId (session)</span>
<input
value={conversationId}
onChange={(e) => setConversationId(e.target.value)}
maxLength={242}
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm font-mono w-52"
/>
</label>
{selectedSdk?.mode === "middleware" && (
<>
<label className="space-y-1">
<span className="text-xs text-zinc-500">Memory mode</span>
<select
value={memoryMode}
onChange={(e) =>
setMemoryMode(
e.target.value as "profile" | "query" | "full",
)
}
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm"
>
<option value="profile">profile</option>
<option value="query">query</option>
<option value="full">full</option>
</select>
</label>
<label className="space-y-1">
<span className="text-xs text-zinc-500">addMemory</span>
<select
value={middlewareConfig.addMemory}
onChange={(e) =>
setMiddlewareConfig((prev) => ({
...prev,
addMemory: e.target.value as "always" | "never",
}))
}
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm"
>
<option value="always">always</option>
<option value="never">never</option>
</select>
</label>
<label className="flex items-end gap-2 pb-1.5 text-xs text-zinc-400">
<input
type="checkbox"
checked={middlewareConfig.verbose}
onChange={(e) =>
setMiddlewareConfig((prev) => ({
...prev,
verbose: e.target.checked,
}))
}
/>
verbose
</label>
{selectedSdk.id === "ts-ai-sdk-middleware" && (
<>
<label className="flex items-end gap-2 pb-1.5 text-xs text-zinc-400">
<input
type="checkbox"
checked={middlewareConfig.includeToolCalls}
onChange={(e) =>
setMiddlewareConfig((prev) => ({
...prev,
includeToolCalls: e.target.checked,
}))
}
/>
includeToolCalls
</label>
<label className="flex items-end gap-2 pb-1.5 text-xs text-zinc-400">
<input
type="checkbox"
checked={middlewareConfig.skipMemoryOnError}
onChange={(e) =>
setMiddlewareConfig((prev) => ({
...prev,
skipMemoryOnError: e.target.checked,
}))
}
/>
skipMemoryOnError
</label>
</>
)}
</>
)}
</div>
<div className="min-h-0 flex-1 overflow-y-auto rounded-lg border border-zinc-800 bg-zinc-900/30 p-4 space-y-3">
{messages.length === 0 && (
<p className="text-center text-sm text-zinc-500 py-8">
Say hi try &quot;Remember that I prefer oat milk&quot; or
&quot;What do you know about me?&quot;
</p>
)}
{messages.map((m, i) => {
if (m.kind === "debug") {
return (
<div key={i} className="flex justify-start">
<div className="max-w-[92%] rounded-lg border border-violet-900/50 bg-violet-950/25 px-3 py-2 text-xs">
<div className="font-medium text-violet-300 mb-1">
Debug · {m.entry.label}
</div>
{m.entry.detail && (
<pre className="font-mono text-violet-100/70 whitespace-pre-wrap break-all mb-2">
{JSON.stringify(m.entry.detail, null, 2)}
</pre>
)}
{m.entry.preview && (
<pre className="font-mono text-zinc-300 whitespace-pre-wrap break-all border-t border-violet-900/40 pt-2 mt-1">
{m.entry.preview}
</pre>
)}
</div>
</div>
)
}
if (m.kind === "tool") {
return (
<div key={i} className="flex justify-start">
<div className="max-w-[90%] rounded-lg border border-amber-900/50 bg-amber-950/30 px-3 py-2 text-xs font-mono text-amber-100/90">
<div className="font-sans text-amber-400 font-medium mb-1">
Tool · step {m.entry.step} · {m.entry.toolName}
</div>
<div className="text-zinc-400">args</div>
<pre className="whitespace-pre-wrap break-all mb-2">
{JSON.stringify(m.entry.args, null, 2)}
</pre>
{m.entry.result !== undefined && (
<>
<div className="text-zinc-400">result</div>
<pre className="whitespace-pre-wrap break-all">
{JSON.stringify(m.entry.result, null, 2)}
</pre>
</>
)}
</div>
</div>
)
}
return (
<div
key={i}
className={
m.kind === "user"
? "flex justify-end"
: "flex justify-start"
}
>
<div
className={`max-w-[85%] rounded-2xl px-4 py-2 text-sm leading-relaxed ${
m.kind === "user"
? "bg-emerald-700 text-white"
: "bg-zinc-800 text-zinc-100"
}`}
>
{m.content}
</div>
</div>
)
})}
{loading && (
<div className="text-sm text-zinc-500 animate-pulse">
Thinking
</div>
)}
</div>
{error && (
<div className="rounded-md border border-red-900 bg-red-950/50 px-3 py-2 text-sm text-red-300">
{error}
</div>
)}
<div className="flex gap-2 shrink-0">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
send()
}
}}
disabled={loading || !selectedSdk?.available || !keysReady}
placeholder={
keysReady
? "Message the agent…"
: "Enter API keys above to chat…"
}
className="flex-1 rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-600/50 disabled:opacity-50"
/>
<button
type="button"
onClick={send}
disabled={
loading ||
!input.trim() ||
!selectedSdk?.available ||
!keysReady
}
className="rounded-lg bg-emerald-600 px-5 py-3 text-sm font-medium text-white hover:bg-emerald-500 disabled:opacity-40"
>
Send
</button>
</div>
</div>
<aside className="hidden w-80 shrink-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/20 p-3 xl:flex xl:w-96 xl:flex-col min-h-0">
<ContextPanel
containerTag={containerTag}
lastUserMessage={lastUserMessage}
refreshKey={contextRefreshKey}
supermemoryApiKey={supermemoryApiKey}
supermemoryKeyReady={supermemoryKeyReady}
/>
</aside>
</div>
</div>
)
}
function Status({ ok, label }: { ok: boolean; label: string }) {
return (
<span
className={`rounded-full border px-3 py-1 ${
ok
? "border-emerald-800 text-emerald-400"
: "border-zinc-700 text-zinc-500"
}`}
>
{label}
</span>
)
}
function Section({
title,
children,
}: {
title: string
children: React.ReactNode
}) {
return (
<div>
<h2 className="mb-2 text-xs font-medium uppercase tracking-wider text-zinc-500">
{title}
</h2>
{children}
</div>
)
}
function SdkButtons({
sdks,
selected,
onSelect,
}: {
sdks: ChatSdkDefinition[]
selected: string
onSelect: (id: string) => void
}) {
return (
<ul className="space-y-1">
{sdks.map((sdk) => (
<li key={sdk.id}>
<button
type="button"
disabled={!sdk.available}
onClick={() => onSelect(sdk.id)}
className={`w-full rounded-md px-2 py-2 text-left text-sm transition-colors ${
selected === sdk.id
? "bg-zinc-800 text-white"
: "text-zinc-400 hover:bg-zinc-900 hover:text-zinc-200"
} ${!sdk.available ? "opacity-40 cursor-not-allowed" : ""}`}
>
<div>{sdk.label}</div>
<div className="text-[10px] text-zinc-500 capitalize">
{sdk.mode}
</div>
</button>
</li>
))}
</ul>
)
}
function SidebarTab({
active,
onClick,
children,
}: {
active: boolean
onClick: () => void
children: React.ReactNode
}) {
return (
<button
type="button"
onClick={onClick}
className={`flex-1 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
active
? "bg-zinc-800 text-white"
: "text-zinc-500 hover:bg-zinc-900 hover:text-zinc-300"
}`}
>
{children}
</button>
)
}

View file

@ -0,0 +1,140 @@
"use client"
import { useEffect, useState } from "react"
import {
clearStoredApiKeys,
readStoredApiKeys,
storeApiKeys,
} from "@/lib/api-keys"
export function ApiKeysPanel({
supermemoryApiKey,
openaiApiKey,
hasSupermemoryEnvKey,
hasOpenAiEnvKey,
onSupermemoryChange,
onOpenAiChange,
}: {
supermemoryApiKey: string
openaiApiKey: string
hasSupermemoryEnvKey: boolean
hasOpenAiEnvKey: boolean
onSupermemoryChange: (value: string) => void
onOpenAiChange: (value: string) => void
}) {
const [open, setOpen] = useState(false)
const [storageInitialized, setStorageInitialized] = useState(false)
const [rememberKeys, setRememberKeys] = useState(false)
useEffect(() => {
const stored = readStoredApiKeys()
const hasStoredKeys = Boolean(
stored.supermemoryApiKey || stored.openaiApiKey,
)
if (stored.supermemoryApiKey) onSupermemoryChange(stored.supermemoryApiKey)
if (stored.openaiApiKey) onOpenAiChange(stored.openaiApiKey)
setRememberKeys(hasStoredKeys)
setStorageInitialized(true)
}, [onSupermemoryChange, onOpenAiChange])
useEffect(() => {
if (!storageInitialized) return
if (rememberKeys) {
storeApiKeys({ supermemoryApiKey, openaiApiKey })
} else {
clearStoredApiKeys()
}
}, [storageInitialized, rememberKeys, supermemoryApiKey, openaiApiKey])
const supermemoryReady =
supermemoryApiKey.trim().length > 0 || hasSupermemoryEnvKey
const openAiReady = openaiApiKey.trim().length > 0 || hasOpenAiEnvKey
const ready = supermemoryReady && openAiReady
return (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm"
>
<span className="font-medium text-zinc-200">API keys</span>
<span className="flex items-center gap-2 text-xs">
<span className={ready ? "text-emerald-400" : "text-amber-400"}>
{ready ? "configured" : "required for chat"}
</span>
<span className="text-zinc-600">{open ? "" : "+"}</span>
</span>
</button>
{open && (
<div className="space-y-3 border-t border-zinc-800 px-3 pb-3 pt-2">
<p className="rounded border border-amber-900/60 bg-amber-950/30 px-2 py-1.5 text-[11px] leading-snug text-amber-200/80">
Use disposable development or test keys onlynever production
credentials. Keys stay in this page only unless you explicitly
enable tab-scoped storage below. Server env vars remain available as
fallbacks.
</p>
<label className="block space-y-1">
<span className="flex items-center justify-between gap-2 text-xs text-zinc-500">
<span>Supermemory API key</span>
{hasSupermemoryEnvKey && !supermemoryApiKey.trim() && (
<span className="text-emerald-500">using server env</span>
)}
</span>
<input
type="password"
value={supermemoryApiKey}
onChange={(e) => onSupermemoryChange(e.target.value)}
placeholder={
hasSupermemoryEnvKey ? "Optional browser override" : "sm_…"
}
className="w-full rounded-md border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-sm font-mono"
/>
</label>
<label className="block space-y-1">
<span className="flex items-center justify-between gap-2 text-xs text-zinc-500">
<span>OpenAI API key</span>
{hasOpenAiEnvKey && !openaiApiKey.trim() && (
<span className="text-emerald-500">using server env</span>
)}
</span>
<input
type="password"
value={openaiApiKey}
onChange={(e) => onOpenAiChange(e.target.value)}
placeholder={
hasOpenAiEnvKey ? "Optional browser override" : "sk-…"
}
className="w-full rounded-md border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-sm font-mono"
/>
</label>
<label className="flex items-start gap-2 text-[11px] leading-snug text-zinc-400">
<input
type="checkbox"
checked={rememberKeys}
onChange={(event) => setRememberKeys(event.target.checked)}
className="mt-0.5"
/>
<span>
Remember these keys for this tab using sessionStorage. They are
cleared when the tab closes; only enable this on a trusted
profile.
</span>
</label>
<button
type="button"
onClick={() => {
onSupermemoryChange("")
onOpenAiChange("")
setRememberKeys(false)
clearStoredApiKeys()
}}
className="text-xs text-zinc-500 hover:text-zinc-300"
>
Clear entered keys
</button>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,350 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import type { ContainerContext } from "@/lib/context-api"
type ContextDocument = ContainerContext["documents"][number]
export function ContextPanel({
containerTag,
lastUserMessage,
refreshKey,
supermemoryApiKey,
supermemoryKeyReady,
}: {
containerTag: string
lastUserMessage?: string
refreshKey: number
supermemoryApiKey: string
supermemoryKeyReady: boolean
}) {
const [context, setContext] = useState<ContainerContext | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [useQuery, setUseQuery] = useState(false)
const [selectedDocKey, setSelectedDocKey] = useState<string | null>(null)
const activeRequest = useRef<AbortController | null>(null)
const load = useCallback(async () => {
if (!supermemoryKeyReady) {
setError("Enter a Supermemory API key or set SUPERMEMORY_API_KEY")
setContext(null)
return
}
const normalizedContainerTag = containerTag.trim()
if (!normalizedContainerTag) {
setError("Enter a container tag to load context")
setContext(null)
return
}
activeRequest.current?.abort()
const controller = new AbortController()
activeRequest.current = controller
setLoading(true)
setError(null)
try {
const res = await fetch("/api/context", {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: controller.signal,
body: JSON.stringify({
containerTag: normalizedContainerTag,
...(useQuery && lastUserMessage ? { query: lastUserMessage } : {}),
...(supermemoryApiKey.trim()
? {
apiKeys: {
supermemoryApiKey: supermemoryApiKey.trim(),
},
}
: {}),
}),
})
const data = await res.json()
if (!data.ok) throw new Error(data.error ?? "Failed to load context")
if (controller.signal.aborted) return
setContext(data.context)
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return
setError(err instanceof Error ? err.message : String(err))
setContext(null)
} finally {
if (activeRequest.current === controller) {
activeRequest.current = null
setLoading(false)
}
}
}, [
containerTag,
lastUserMessage,
useQuery,
supermemoryApiKey,
supermemoryKeyReady,
])
useEffect(() => {
// The counter changes after a successful chat and explicitly refreshes context.
void refreshKey
if (!supermemoryKeyReady || !containerTag.trim()) {
activeRequest.current?.abort()
activeRequest.current = null
setLoading(false)
setContext(null)
setError(null)
return
}
const timeout = window.setTimeout(() => {
void load()
}, 500)
return () => {
window.clearTimeout(timeout)
activeRequest.current?.abort()
}
}, [load, refreshKey, supermemoryKeyReady, containerTag])
useEffect(() => {
// A different container must not retain the previous document selection.
void containerTag
setSelectedDocKey(null)
}, [containerTag])
const selectedDoc =
context?.documents.find((doc) => documentKey(doc) === selectedDocKey) ??
null
return (
<div className="flex min-h-0 flex-col gap-3 text-sm">
<div className="flex items-center justify-between gap-2">
<h2 className="text-xs font-medium uppercase tracking-wider text-zinc-500">
Container context
</h2>
<button
type="button"
onClick={() => void load()}
disabled={!supermemoryKeyReady || !containerTag.trim()}
className="text-xs text-zinc-400 hover:text-zinc-200 disabled:cursor-not-allowed disabled:text-zinc-700"
>
Refresh
</button>
</div>
<label className="flex items-center gap-2 text-xs text-zinc-400">
<input
type="checkbox"
checked={useQuery}
onChange={(e) => setUseQuery(e.target.checked)}
/>
Profile with last message as query
</label>
{loading && <p className="text-xs text-zinc-500">Loading</p>}
{!supermemoryKeyReady && (
<p className="text-xs text-zinc-500">
Enter a Supermemory key or set SUPERMEMORY_API_KEY to load context.
</p>
)}
{error && <p className="text-xs text-red-400">{error}</p>}
{context && (
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto pr-1">
<section>
<h3 className="mb-2 text-xs font-medium text-zinc-400">
Profile · {context.profile.static.length} static ·{" "}
{context.profile.dynamic.length} dynamic ·{" "}
{context.profile.searchResults.length} search
</h3>
<div className="space-y-2">
<MemoryList title="Static" items={context.profile.static} />
<MemoryList title="Dynamic" items={context.profile.dynamic} />
{context.profile.searchResults.length > 0 && (
<MemoryList
title="Search results"
items={context.profile.searchResults}
/>
)}
</div>
</section>
<section className="min-h-0">
<h3 className="mb-2 text-xs font-medium text-zinc-400">
Documents / sessions ({context.documents.length})
</h3>
{context.documents.length === 0 ? (
<p className="text-xs text-zinc-500">No documents yet</p>
) : (
<div className="flex min-h-0 gap-2">
<ul className="min-w-0 flex-1 space-y-2">
{context.documents.map((doc) => {
const key = documentKey(doc)
const isSelected = key === selectedDocKey
const memoryCount = doc.memoryEntries?.length ?? 0
return (
<li key={key}>
<button
type="button"
onClick={() =>
setSelectedDocKey(isSelected ? null : key)
}
className={`w-full rounded border p-2 text-left text-xs transition-colors ${
isSelected
? "border-emerald-700/60 bg-emerald-950/30"
: "border-zinc-800 bg-zinc-900/50 hover:border-zinc-700"
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 font-mono text-zinc-300 truncate">
{doc.id ?? "—"}
</div>
<span
className={`shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-medium tabular-nums ${
memoryCount > 0
? "bg-emerald-900/50 text-emerald-300"
: "bg-zinc-800 text-zinc-500"
}`}
>
{memoryCount}
</span>
</div>
<div className="text-zinc-500 truncate">
{doc.title ?? "untitled"}
</div>
<div className="text-zinc-600">
{doc.customId
? `session: ${doc.customId}`
: "no customId"}
{doc.status ? ` · ${doc.status}` : ""}
</div>
</button>
</li>
)
})}
</ul>
{selectedDoc && (
<div className="min-w-0 flex-1 border-l border-zinc-800 pl-2">
<DocumentMemoriesPanel doc={selectedDoc} />
</div>
)}
</div>
)}
{context.documents.length > 0 && !selectedDoc && (
<p className="mt-2 text-[10px] text-zinc-600">
Click a document to view its memories
</p>
)}
</section>
</div>
)}
</div>
)
}
function DocumentMemoriesPanel({ doc }: { doc: ContextDocument }) {
const entries = doc.memoryEntries ?? []
return (
<div className="space-y-2">
<div className="text-[10px] uppercase tracking-wide text-zinc-600">
Document memories ({entries.length})
</div>
<div className="text-xs text-zinc-500 truncate">
{doc.title ?? "untitled"}
</div>
{doc.summary && (
<p className="text-[11px] leading-snug text-zinc-500 line-clamp-3">
{doc.summary}
</p>
)}
{entries.length === 0 ? (
<p className="text-xs text-zinc-600">No memories on this document</p>
) : (
<ul className="space-y-2 max-h-64 overflow-y-auto pr-1">
{entries.map((entry, i) => (
<li
key={memoryEntryKey(entry, i)}
className="rounded border border-zinc-800 bg-zinc-900/60 p-2"
>
<MemoryEntryCard entry={entry} />
</li>
))}
</ul>
)}
</div>
)
}
function MemoryEntryCard({ entry }: { entry: unknown }) {
const record =
entry && typeof entry === "object"
? (entry as Record<string, unknown>)
: null
const memoryText = formatMemoryItem(entry)
const id = record?.id as string | undefined
const version = record?.version as number | undefined
const isForgotten = Boolean(record?.isForgotten)
const isStatic = Boolean(record?.isStatic)
const forgetAfter = record?.forgetAfter as string | undefined
return (
<div className="space-y-1">
{memoryText && (
<p className="text-xs leading-snug text-zinc-300">{memoryText}</p>
)}
<div className="flex flex-wrap gap-1 text-[10px] text-zinc-600">
{id && <span className="font-mono truncate max-w-full">{id}</span>}
{version != null && <span>v{version}</span>}
{isStatic && <span className="text-sky-500">static</span>}
{isForgotten && <span className="text-amber-500">forgotten</span>}
{forgetAfter && !isForgotten && (
<span className="text-orange-500">expires</span>
)}
</div>
</div>
)
}
function documentKey(doc: ContextDocument): string {
return doc.id ?? doc.customId ?? doc.title ?? "unknown"
}
function memoryEntryKey(entry: unknown, index: number): string {
if (entry && typeof entry === "object" && "id" in entry) {
return String((entry as { id: unknown }).id)
}
return `memory-${index}`
}
function MemoryList({ title, items }: { title: string; items: unknown[] }) {
if (!items.length) return null
return (
<div>
<div className="text-[10px] uppercase tracking-wide text-zinc-600 mb-1">
{title}
</div>
<ul className="space-y-1">
{items.slice(0, 12).map((item, i) => (
<li
key={i}
className="rounded bg-zinc-900/60 px-2 py-1 text-xs text-zinc-300 leading-snug"
>
{formatMemoryItem(item)}
</li>
))}
</ul>
</div>
)
}
function formatMemoryItem(item: unknown): string {
if (typeof item === "string") return item
if (item && typeof item === "object") {
const record = item as Record<string, unknown>
if (typeof record.memory === "string") return record.memory
if (typeof record.content === "string") return record.content
if (typeof record.chunk === "string") return record.chunk
}
return JSON.stringify(item)
}

View file

@ -0,0 +1,122 @@
"use client"
import { useState } from "react"
import { TOOL_CATALOG, type CatalogTool } from "@/lib/tools-catalog"
export function ToolsReferencePanel() {
const [expandedId, setExpandedId] = useState<string | null>("documentAdd")
return (
<div className="flex min-h-0 flex-col gap-2 text-sm">
<div>
<h2 className="text-xs font-medium uppercase tracking-wider text-zinc-500">
Tool reference
</h2>
<p className="mt-1 text-[10px] leading-snug text-zinc-600">
Canonical descriptions from{" "}
<code className="text-zinc-500">@supermemory/tools</code> what the
model sees in tools mode.
</p>
</div>
<ul className="min-h-0 flex-1 space-y-2 overflow-y-auto pr-1">
{TOOL_CATALOG.map((tool) => (
<ToolCard
key={tool.id}
tool={tool}
expanded={expandedId === tool.id}
onToggle={() =>
setExpandedId((id) => (id === tool.id ? null : tool.id))
}
/>
))}
</ul>
</div>
)
}
function ToolCard({
tool,
expanded,
onToggle,
}: {
tool: CatalogTool
expanded: boolean
onToggle: () => void
}) {
return (
<li className="rounded border border-zinc-800 bg-zinc-900/40">
<button
type="button"
onClick={onToggle}
className="w-full px-2 py-2 text-left"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="font-mono text-xs text-emerald-400/90">
{tool.id}
</div>
<div className="font-mono text-[10px] text-zinc-600">
py: {tool.pythonName}
</div>
</div>
<span className="shrink-0 text-[10px] text-zinc-600">
{expanded ? "" : "+"}
</span>
</div>
{!expanded && (
<p className="mt-1 line-clamp-2 text-[11px] leading-snug text-zinc-500">
{tool.description}
</p>
)}
</button>
{expanded && (
<div className="border-t border-zinc-800 px-2 pb-2 pt-1 space-y-2">
<p className="text-[11px] leading-relaxed text-zinc-300">
{tool.description}
</p>
{tool.parameters.length > 0 && (
<div>
<div className="text-[10px] uppercase tracking-wide text-zinc-600 mb-1">
Parameters
</div>
<ul className="space-y-1.5">
{tool.parameters.map((param) => (
<li
key={param.name}
className="rounded bg-zinc-950/60 px-2 py-1"
>
<div className="flex flex-wrap items-center gap-1">
<span className="font-mono text-[10px] text-sky-400/90">
{param.name}
</span>
{param.pythonName && param.pythonName !== param.name && (
<span className="font-mono text-[10px] text-zinc-600">
/ {param.pythonName}
</span>
)}
{!param.pythonName && (
<span className="text-[9px] text-sky-500/80">
TypeScript only
</span>
)}
{param.required && (
<span className="text-[9px] text-amber-500/80">
required
</span>
)}
</div>
<p className="mt-0.5 text-[10px] leading-snug text-zinc-500">
{param.description}
</p>
</li>
))}
</ul>
</div>
)}
</div>
)}
</li>
)
}

View file

@ -0,0 +1,67 @@
export interface PlaygroundApiKeys {
supermemoryApiKey: string
openaiApiKey: string
}
export const API_KEYS_STORAGE_KEY = "sdk-playground-api-keys"
interface ResolveApiKeyOptions {
allowEnvironment?: boolean
}
export function resolveSupermemoryApiKey(
input?: Partial<PlaygroundApiKeys> | null,
options: ResolveApiKeyOptions = {},
): string | null {
const provided = input?.supermemoryApiKey?.trim()
if (provided) return provided
if (options.allowEnvironment === false) return null
return process.env.SUPERMEMORY_API_KEY?.trim() || null
}
export function resolveOpenAiApiKey(
input?: Partial<PlaygroundApiKeys> | null,
options: ResolveApiKeyOptions = {},
): string | null {
const provided = input?.openaiApiKey?.trim()
if (provided) return provided
if (options.allowEnvironment === false) return null
return process.env.OPENAI_API_KEY?.trim() || null
}
export function resolveApiKeys(
input?: Partial<PlaygroundApiKeys> | null,
options: ResolveApiKeyOptions = {},
): PlaygroundApiKeys | null {
const supermemoryApiKey = resolveSupermemoryApiKey(input, options)
const openaiApiKey = resolveOpenAiApiKey(input, options)
if (!supermemoryApiKey || !openaiApiKey) return null
return { supermemoryApiKey, openaiApiKey }
}
export function readStoredApiKeys(): Partial<PlaygroundApiKeys> {
if (typeof window === "undefined") return {}
try {
const raw = sessionStorage.getItem(API_KEYS_STORAGE_KEY)
if (!raw) return {}
const parsed = JSON.parse(raw) as Partial<PlaygroundApiKeys>
return {
supermemoryApiKey: parsed.supermemoryApiKey ?? "",
openaiApiKey: parsed.openaiApiKey ?? "",
}
} catch {
return {}
}
}
export function storeApiKeys(keys: Partial<PlaygroundApiKeys>) {
if (typeof window === "undefined") return
sessionStorage.setItem(API_KEYS_STORAGE_KEY, JSON.stringify(keys))
}
export function clearStoredApiKeys() {
if (typeof window === "undefined") return
sessionStorage.removeItem(API_KEYS_STORAGE_KEY)
}

View file

@ -0,0 +1,437 @@
import { createOpenAI } from "@ai-sdk/openai"
import { generateText, stepCountIs, type ModelMessage } from "ai"
import OpenAI from "openai"
import { supermemoryTools as aiSdkPackageTools } from "@supermemory/ai-sdk"
import { withSupermemory as withSupermemoryAiSdk } from "@supermemory/tools/ai-sdk"
import {
createToolCallsExecutor,
getToolDefinitions,
withSupermemory as withSupermemoryOpenAi,
} from "@supermemory/tools/openai"
import { supermemoryTools as aiSdkTools } from "@supermemory/tools/ai-sdk"
import type { SupermemoryToolsConfig } from "@supermemory/tools"
import type { PlaygroundApiKeys } from "./api-keys"
import {
buildMiddlewareMemoryDebug,
type MemoryDebugEntry,
} from "./context-api"
import {
normalizeMiddlewareConfig,
type MiddlewareRuntimeConfig,
} from "./middleware-config"
import {
TOOLS_SYSTEM_PROMPT,
getChatSdk,
type ToolTraceEntry,
} from "./sdk-registry"
export type ChatMessage = {
role: "user" | "assistant" | "system"
content: string
}
export interface ChatResult {
text: string
toolTrace: ToolTraceEntry[]
memoryDebug: MemoryDebugEntry[]
}
export interface ChatRequest {
sdkId: string
messages: ChatMessage[]
containerTag: string
conversationId: string
memoryMode?: "profile" | "query" | "full"
middlewareConfig?: Partial<MiddlewareRuntimeConfig>
apiKeys?: Partial<PlaygroundApiKeys>
containerTags?: string[]
projectId?: string
}
export class PlaygroundChatTimeoutError extends Error {
constructor(message: string) {
super(message)
this.name = "PlaygroundChatTimeoutError"
}
}
const MODEL_REQUEST_TIMEOUT_MS = 120_000
const DEBUG_REQUEST_TIMEOUT_MS = 10_000
const MAX_OUTPUT_TOKENS = 2_048
async function withChatDeadline<T>(
operation: (signal: AbortSignal) => Promise<T>,
): Promise<T> {
const controller = new AbortController()
let timeout: ReturnType<typeof setTimeout> | undefined
const deadline = new Promise<never>((_resolve, reject) => {
timeout = setTimeout(() => {
const error = new PlaygroundChatTimeoutError(
`TypeScript chat timed out after ${MODEL_REQUEST_TIMEOUT_MS / 1_000} seconds`,
)
controller.abort(error)
reject(error)
}, MODEL_REQUEST_TIMEOUT_MS)
})
try {
return await Promise.race([operation(controller.signal), deadline])
} finally {
if (timeout) clearTimeout(timeout)
}
}
async function buildBestEffortDebug(
operation: (signal: AbortSignal) => Promise<MemoryDebugEntry[]>,
): Promise<MemoryDebugEntry[]> {
const controller = new AbortController()
let timeout: ReturnType<typeof setTimeout> | undefined
const deadline = new Promise<MemoryDebugEntry[]>((resolve) => {
timeout = setTimeout(() => {
controller.abort()
resolve([
{
type: "debug_error",
label: "Post-response context reconstruction timed out",
detail: { nonFatal: true },
},
])
}, DEBUG_REQUEST_TIMEOUT_MS)
})
try {
return await Promise.race([operation(controller.signal), deadline])
} catch {
return [
{
type: "debug_error",
label: "Post-response context reconstruction unavailable",
detail: { nonFatal: true },
},
]
} finally {
if (timeout) clearTimeout(timeout)
}
}
function getModelName(): string {
return process.env.MODEL_NAME ?? "gpt-4o-mini"
}
function getToolsConfig(
containerTags?: string[],
projectId?: string,
): SupermemoryToolsConfig {
return {
baseUrl: process.env.SUPERMEMORY_BASE_URL,
...(containerTags?.length ? { containerTags } : {}),
...(projectId ? { projectId } : {}),
}
}
function toModelMessages(messages: ChatMessage[]): ModelMessage[] {
return messages.map((m) => ({ role: m.role, content: m.content }))
}
function toOpenAiMessages(
messages: ChatMessage[],
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
return messages.map((m) => ({ role: m.role, content: m.content }))
}
function extractAiSdkToolTrace(
steps: Array<{
toolCalls: Array<{ toolName: string; input: unknown }>
toolResults: Array<{ toolName: string; output: unknown }>
}>,
): ToolTraceEntry[] {
const trace: ToolTraceEntry[] = []
for (const [stepIndex, step] of steps.entries()) {
for (let i = 0; i < step.toolCalls.length; i++) {
const call = step.toolCalls[i]
const result = step.toolResults[i]
trace.push({
step: stepIndex + 1,
toolName: call.toolName,
args: call.input,
result: result?.output,
})
}
}
return trace
}
function lastUserMessage(messages: ChatMessage[]): string {
return [...messages].reverse().find((m) => m.role === "user")?.content ?? ""
}
async function chatAiSdkMiddleware(
keys: PlaygroundApiKeys,
messages: ChatMessage[],
containerTag: string,
conversationId: string,
memoryMode: "profile" | "query" | "full",
middlewareConfig: MiddlewareRuntimeConfig,
signal: AbortSignal,
): Promise<ChatResult> {
const openai = createOpenAI({ apiKey: keys.openaiApiKey })
const model = withSupermemoryAiSdk(openai(getModelName()), {
containerTag,
customId: conversationId,
apiKey: keys.supermemoryApiKey,
mode: memoryMode,
addMemory: middlewareConfig.addMemory,
verbose: middlewareConfig.verbose,
includeToolCalls: middlewareConfig.includeToolCalls,
skipMemoryOnError: middlewareConfig.skipMemoryOnError,
baseUrl: process.env.SUPERMEMORY_BASE_URL,
})
const result = await generateText({
model,
system: "You are a helpful assistant with long-term memory about the user.",
messages: toModelMessages(messages.filter((m) => m.role !== "system")),
maxOutputTokens: MAX_OUTPUT_TOKENS,
abortSignal: signal,
})
return { text: result.text, toolTrace: [], memoryDebug: [] }
}
async function chatOpenAiMiddleware(
keys: PlaygroundApiKeys,
messages: ChatMessage[],
containerTag: string,
conversationId: string,
memoryMode: "profile" | "query" | "full",
middlewareConfig: MiddlewareRuntimeConfig,
signal: AbortSignal,
): Promise<ChatResult> {
const openai = new OpenAI({
apiKey: keys.openaiApiKey,
timeout: MODEL_REQUEST_TIMEOUT_MS,
maxRetries: 1,
})
const client = withSupermemoryOpenAi(openai, {
containerTag,
customId: conversationId,
apiKey: keys.supermemoryApiKey,
mode: memoryMode,
addMemory: middlewareConfig.addMemory,
verbose: middlewareConfig.verbose,
baseUrl: process.env.SUPERMEMORY_BASE_URL,
})
const response = await client.chat.completions.create(
{
model: getModelName(),
messages: toOpenAiMessages(messages),
max_tokens: MAX_OUTPUT_TOKENS,
},
{ signal },
)
return {
text: response.choices[0]?.message?.content ?? "",
toolTrace: [],
memoryDebug: [],
}
}
async function chatAiSdkTools(
keys: PlaygroundApiKeys,
toolsFactory: typeof aiSdkTools,
messages: ChatMessage[],
containerTags?: string[],
projectId?: string,
signal?: AbortSignal,
): Promise<ChatResult> {
const openai = createOpenAI({ apiKey: keys.openaiApiKey })
const tools = toolsFactory(
keys.supermemoryApiKey,
getToolsConfig(containerTags, projectId),
)
const result = await generateText({
model: openai(getModelName()),
system: TOOLS_SYSTEM_PROMPT,
messages: toModelMessages(messages.filter((m) => m.role !== "system")),
tools,
stopWhen: stepCountIs(8),
maxOutputTokens: MAX_OUTPUT_TOKENS,
abortSignal: signal,
})
return {
text: result.text,
toolTrace: extractAiSdkToolTrace(result.steps),
memoryDebug: [],
}
}
async function chatOpenAiTools(
keys: PlaygroundApiKeys,
messages: ChatMessage[],
containerTags?: string[],
projectId?: string,
signal?: AbortSignal,
): Promise<ChatResult> {
const openai = new OpenAI({
apiKey: keys.openaiApiKey,
timeout: MODEL_REQUEST_TIMEOUT_MS,
maxRetries: 1,
})
const config = getToolsConfig(containerTags, projectId)
const executeToolCalls = createToolCallsExecutor(
keys.supermemoryApiKey,
config,
)
const toolDefs = getToolDefinitions()
const trace: ToolTraceEntry[] = []
const convo: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{ role: "system", content: TOOLS_SYSTEM_PROMPT },
...toOpenAiMessages(messages.filter((m) => m.role !== "system")),
]
for (let step = 0; step < 8; step++) {
const response = await openai.chat.completions.create(
{
model: getModelName(),
messages: convo,
tools: toolDefs,
max_tokens: MAX_OUTPUT_TOKENS,
},
{ signal },
)
const choice = response.choices[0]?.message
if (!choice) break
convo.push(choice)
if (choice.tool_calls?.length) {
const toolMessages = await executeToolCalls(choice.tool_calls)
for (let i = 0; i < choice.tool_calls.length; i++) {
const call = choice.tool_calls[i]
const rawContent = toolMessages[i]?.content
const raw =
typeof rawContent === "string"
? rawContent
: JSON.stringify(rawContent)
let parsedResult: unknown = raw
try {
parsedResult = JSON.parse(raw)
} catch {
/* keep string */
}
trace.push({
step: step + 1,
toolName: call.function.name,
args: JSON.parse(call.function.arguments),
result: parsedResult,
})
}
convo.push(...toolMessages)
continue
}
return { text: choice.content ?? "", toolTrace: trace, memoryDebug: [] }
}
throw new Error("Tool loop exceeded max steps")
}
export async function runTypeScriptChat(
request: ChatRequest,
keys: PlaygroundApiKeys,
): Promise<ChatResult> {
const sdk = getChatSdk(request.sdkId)
if (!sdk || sdk.language !== "typescript" || !sdk.available) {
throw new Error(`Invalid TypeScript chat SDK: ${request.sdkId}`)
}
const containerTags = request.containerTags ?? [request.containerTag]
const memoryMode = request.memoryMode ?? "full"
const middlewareConfig = normalizeMiddlewareConfig(request.middlewareConfig)
const result = await withChatDeadline(async (signal) => {
switch (request.sdkId) {
case "ts-ai-sdk-middleware":
return await chatAiSdkMiddleware(
keys,
request.messages,
request.containerTag,
request.conversationId,
memoryMode,
middlewareConfig,
signal,
)
case "ts-openai-middleware":
return await chatOpenAiMiddleware(
keys,
request.messages,
request.containerTag,
request.conversationId,
memoryMode,
middlewareConfig,
signal,
)
case "ts-ai-sdk-tools":
return await chatAiSdkTools(
keys,
aiSdkTools,
request.messages,
containerTags,
request.projectId,
signal,
)
case "ts-openai-tools":
return await chatOpenAiTools(
keys,
request.messages,
containerTags,
request.projectId,
signal,
)
case "ts-ai-sdk-package":
return await chatAiSdkTools(
keys,
aiSdkPackageTools,
request.messages,
containerTags,
request.projectId,
signal,
)
default:
throw new Error(`Unhandled SDK: ${request.sdkId}`)
}
})
if (
request.sdkId !== "ts-ai-sdk-middleware" &&
request.sdkId !== "ts-openai-middleware"
) {
return result
}
const memoryDebug = await buildBestEffortDebug((signal) =>
buildMiddlewareMemoryDebug(
request.containerTag,
request.conversationId,
memoryMode,
lastUserMessage(request.messages),
middlewareConfig,
request.sdkId === "ts-ai-sdk-middleware"
? {
includeToolCalls: middlewareConfig.includeToolCalls,
skipMemoryOnError: middlewareConfig.skipMemoryOnError,
}
: undefined,
keys.supermemoryApiKey,
signal,
),
)
return { ...result, memoryDebug }
}

View file

@ -0,0 +1,313 @@
import Supermemory from "supermemory"
import {
type MiddlewareRuntimeConfig,
normalizeMiddlewareConfig,
} from "./middleware-config"
export interface MemoryDebugEntry {
type:
| "context_reconstruction"
| "context_preview"
| "conversation_save_requested"
| "conversation_save_accepted"
| "conversation_save_failed"
| "conversation_save_queued"
| "conversation_save_skipped"
| "conversation_saved"
| "profile_fetch"
| "context_debug_unavailable"
| "debug_error"
| "manual_profile"
label: string
detail?: Record<string, unknown>
preview?: string
}
export interface ContainerContext {
containerTag: string
query?: string
profile: {
static: unknown[]
dynamic: unknown[]
searchResults: unknown[]
}
documents: Array<{
id?: string
title?: string
status?: string
customId?: string
createdAt?: string
updatedAt?: string
summary?: string
memoryEntries?: unknown[]
}>
pagination?: unknown
}
function getSupermemoryClient(apiKey: string) {
if (!apiKey) throw new Error("Supermemory API key is required")
return new Supermemory({
apiKey,
timeout: 10_000,
maxRetries: 1,
...(process.env.SUPERMEMORY_BASE_URL
? { baseURL: process.env.SUPERMEMORY_BASE_URL }
: {}),
})
}
function normalizeMemoryEntries(record: Record<string, unknown>): unknown[] {
const raw =
record.memoryEntries ??
record.memory_entries ??
(Array.isArray(record.memories) &&
record.memories.length > 0 &&
typeof (record.memories[0] as Record<string, unknown>)?.memory === "string"
? record.memories
: undefined)
return Array.isArray(raw) ? raw : []
}
function memoryText(item: unknown): string {
if (typeof item === "string") return item
if (item && typeof item === "object") {
const record = item as Record<string, unknown>
if (typeof record.memory === "string") return record.memory
if (typeof record.content === "string") return record.content
if (typeof record.chunk === "string") return record.chunk
}
return JSON.stringify(item)
}
function summarizeProfile(profile: ContainerContext["profile"]) {
return {
staticCount: profile.static.length,
dynamicCount: profile.dynamic.length,
searchResultCount: profile.searchResults.length,
staticPreview: profile.static.slice(0, 5).map(memoryText),
dynamicPreview: profile.dynamic.slice(0, 5).map(memoryText),
searchPreview: profile.searchResults.slice(0, 5).map(memoryText),
}
}
function selectProfileForMode(
profile: ContainerContext["profile"],
mode: "profile" | "query" | "full",
): ContainerContext["profile"] {
return {
static: mode === "query" ? [] : profile.static,
dynamic: mode === "query" ? [] : profile.dynamic,
searchResults: mode === "profile" ? [] : profile.searchResults,
}
}
function buildContextPreview(
profile: ContainerContext["profile"],
mode: "profile" | "query" | "full",
query?: string,
): string {
const lines: string[] = [`[memory mode: ${mode}]`]
if (query) lines.push(`[query: ${query}]`)
if (mode !== "query" && profile.static.length) {
lines.push("Static:")
for (const item of profile.static.slice(0, 8)) {
lines.push(`- ${memoryText(item)}`)
}
}
if (mode !== "query" && profile.dynamic.length) {
lines.push("Dynamic:")
for (const item of profile.dynamic.slice(0, 8)) {
lines.push(`- ${memoryText(item)}`)
}
}
if (mode !== "profile" && profile.searchResults.length) {
lines.push("Search results:")
for (const item of profile.searchResults.slice(0, 8)) {
lines.push(`- ${memoryText(item)}`)
}
}
return lines.join("\n")
}
function normalizeSearchResults(searchResults: unknown): unknown[] {
if (!searchResults) return []
if (Array.isArray(searchResults)) return searchResults
if (typeof searchResults === "object") {
const record = searchResults as Record<string, unknown>
if (Array.isArray(record.results)) return record.results
}
return []
}
export function resolveProfileQuery(
lastUserMessage: string,
mode: "profile" | "query" | "full",
): string | undefined {
if (mode === "profile") return undefined
return lastUserMessage || undefined
}
async function fetchProfileContext(
client: ReturnType<typeof getSupermemoryClient>,
containerTag: string,
query?: string,
signal?: AbortSignal,
): Promise<ContainerContext["profile"]> {
const profileResponse = await client.profile(
{
containerTag,
...(query ? { q: query } : {}),
},
{ signal },
)
const profileRaw = profileResponse.profile as
| { static?: unknown[]; dynamic?: unknown[] }
| undefined
return {
static: profileRaw?.static ?? [],
dynamic: profileRaw?.dynamic ?? [],
searchResults: normalizeSearchResults(profileResponse.searchResults),
}
}
export async function fetchContainerContext(
containerTag: string,
query?: string,
supermemoryApiKey?: string,
): Promise<ContainerContext> {
const apiKey =
supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim()
if (!apiKey) throw new Error("Supermemory API key is required")
const client = getSupermemoryClient(apiKey)
const profile = await fetchProfileContext(client, containerTag, query)
const docsResponse = await client.post<{
documents?: unknown[]
pagination?: unknown
}>("/v3/documents/documents", {
body: {
containerTags: [containerTag],
limit: 25,
sort: "createdAt",
order: "desc",
},
})
const rawDocuments = docsResponse.documents ?? []
const documents = rawDocuments.map((doc) => {
const record = doc as Record<string, unknown>
return {
id: record.id as string | undefined,
title: record.title as string | undefined,
status: record.status as string | undefined,
customId: record.customId as string | undefined,
createdAt: record.createdAt as string | undefined,
updatedAt: record.updatedAt as string | undefined,
summary: record.summary as string | undefined,
memoryEntries: normalizeMemoryEntries(record),
}
})
return {
containerTag,
query,
profile,
documents,
pagination: docsResponse.pagination,
}
}
export async function buildMiddlewareMemoryDebug(
containerTag: string,
conversationId: string,
memoryMode: "profile" | "query" | "full",
lastUserMessage: string,
middlewareConfig?: Partial<MiddlewareRuntimeConfig>,
aiSdkExtras?: {
includeToolCalls?: boolean
skipMemoryOnError?: boolean
},
supermemoryApiKey?: string,
signal?: AbortSignal,
): Promise<MemoryDebugEntry[]> {
const config = normalizeMiddlewareConfig(middlewareConfig)
const query = resolveProfileQuery(lastUserMessage, memoryMode)
try {
const apiKey =
supermemoryApiKey?.trim() || process.env.SUPERMEMORY_API_KEY?.trim()
if (!apiKey) throw new Error("Supermemory API key is required")
const profile = await fetchProfileContext(
getSupermemoryClient(apiKey),
containerTag,
query,
signal,
)
const selectedProfile = selectProfileForMode(profile, memoryMode)
const summary = summarizeProfile(selectedProfile)
return [
{
type: "context_reconstruction",
label: "Post-response context reconstruction",
detail: {
authoritativeMiddlewareCapture: false,
timing: "after model response",
endpoint: "POST /v4/profile",
containerTag,
customId: conversationId,
memoryMode,
addMemory: config.addMemory,
verbose: config.verbose,
...(aiSdkExtras?.includeToolCalls !== undefined
? { includeToolCalls: aiSdkExtras.includeToolCalls }
: {}),
...(aiSdkExtras?.skipMemoryOnError !== undefined
? { skipMemoryOnError: aiSdkExtras.skipMemoryOnError }
: {}),
query: query ?? null,
...summary,
},
},
{
type: "context_preview",
label: "Reconstructed context preview (not middleware capture)",
preview: buildContextPreview(selectedProfile, memoryMode, query),
},
config.addMemory === "always"
? {
type: "conversation_save_requested",
label: "Conversation save requested by middleware",
detail: {
confirmed: false,
containerTag,
customId: conversationId,
addMemory: config.addMemory,
verbose: config.verbose,
...(aiSdkExtras?.includeToolCalls !== undefined
? { includeToolCalls: aiSdkExtras.includeToolCalls }
: {}),
},
}
: {
type: "conversation_save_skipped",
label: "Conversation saving disabled",
detail: { addMemory: config.addMemory },
},
]
} catch (error) {
return [
{
type: "debug_error",
label: "Post-response context reconstruction unavailable",
detail: {
nonFatal: true,
error: error instanceof Error ? error.message : String(error),
},
},
]
}
}

View file

@ -0,0 +1,30 @@
export type AddMemoryMode = "always" | "never"
export type MemoryMode = "profile" | "query" | "full"
export interface MiddlewareRuntimeConfig {
addMemory: AddMemoryMode
verbose: boolean
includeToolCalls: boolean
skipMemoryOnError: boolean
}
export const DEFAULT_MIDDLEWARE_CONFIG: MiddlewareRuntimeConfig = {
addMemory: "always",
verbose: false,
includeToolCalls: false,
skipMemoryOnError: true,
}
export function normalizeMiddlewareConfig(
input?: Partial<MiddlewareRuntimeConfig> | null,
): MiddlewareRuntimeConfig {
if (!input) return { ...DEFAULT_MIDDLEWARE_CONFIG }
return {
addMemory: input.addMemory ?? DEFAULT_MIDDLEWARE_CONFIG.addMemory,
verbose: input.verbose ?? DEFAULT_MIDDLEWARE_CONFIG.verbose,
includeToolCalls:
input.includeToolCalls ?? DEFAULT_MIDDLEWARE_CONFIG.includeToolCalls,
skipMemoryOnError:
input.skipMemoryOnError ?? DEFAULT_MIDDLEWARE_CONFIG.skipMemoryOnError,
}
}

View file

@ -0,0 +1,354 @@
import type { PlaygroundApiKeys } from "./api-keys"
import type { ChatMessage } from "./chat-handlers"
import type { MiddlewareRuntimeConfig } from "./middleware-config"
const MAX_BODY_BYTES = 256_000
const MAX_MESSAGES = 64
const MAX_MESSAGE_LENGTH = 20_000
const MAX_TOTAL_MESSAGE_LENGTH = 100_000
const MAX_IDENTIFIER_LENGTH = 256
const MAX_API_KEY_LENGTH = 1_024
const MAX_CONTAINER_TAG_LENGTH = 100
const MAX_CONVERSATION_ID_LENGTH = 242
const CONTAINER_TAG_PATTERN = /^[a-zA-Z0-9_:-]+$/
export class PlaygroundRequestError extends Error {
constructor(
message: string,
readonly status: number,
) {
super(message)
this.name = "PlaygroundRequestError"
}
}
function firstHeaderValue(value: string | null): string | null {
const first = value?.split(",", 1)[0]?.trim()
return first || null
}
function requestOrigin(request: Request): string | null {
const internalUrl = new URL(request.url)
const forwardedHostHeader = request.headers.get("x-forwarded-host")
const forwardedProtoHeader = request.headers.get("x-forwarded-proto")
const hostHeader = request.headers.get("host")
const directHost = firstHeaderValue(hostHeader)
if (hostHeader !== null && !directHost) return null
const forwardedHost = firstHeaderValue(forwardedHostHeader)
if (forwardedHostHeader !== null && !forwardedHost) return null
// Portless preserves the routed Host header but may preserve a client-supplied
// X-Forwarded-Host. Trust Host first so XFH cannot widen env-key access.
const host = directHost ?? forwardedHost
if (!host) return internalUrl.origin
const forwardedProto = firstHeaderValue(forwardedProtoHeader)
if (
forwardedProtoHeader !== null &&
forwardedProto !== "http" &&
forwardedProto !== "https"
) {
return null
}
const protocol =
forwardedProto === "http" || forwardedProto === "https"
? forwardedProto
: internalUrl.protocol.slice(0, -1)
try {
const externalUrl = new URL(`${protocol}://${host}`)
if (
externalUrl.host.toLowerCase() !== host.toLowerCase() ||
externalUrl.username ||
externalUrl.password ||
externalUrl.pathname !== "/" ||
externalUrl.search ||
externalUrl.hash
) {
return null
}
return externalUrl.origin
} catch {
return null
}
}
export function assertTrustedBrowserRequest(request: Request): void {
if (request.headers.get("sec-fetch-site") === "cross-site") {
throw new PlaygroundRequestError("Cross-site requests are not allowed", 403)
}
const expectedOrigin = requestOrigin(request)
if (!expectedOrigin) {
throw new PlaygroundRequestError("Request host is not allowed", 403)
}
const origin = request.headers.get("origin")
let normalizedOrigin: string | null = null
if (origin) {
try {
normalizedOrigin = new URL(origin).origin
} catch {
throw new PlaygroundRequestError("Request origin is not allowed", 403)
}
}
if (normalizedOrigin && normalizedOrigin !== expectedOrigin) {
throw new PlaygroundRequestError("Request origin is not allowed", 403)
}
}
export function mayUseEnvironmentKeys(request: Request): boolean {
if (process.env.SDK_PLAYGROUND_ALLOW_ENV_KEYS === "true") return true
const origin = requestOrigin(request)
if (!origin) return false
const hostname = new URL(origin).hostname.toLowerCase()
return (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "[::1]" ||
hostname.endsWith(".localhost") ||
hostname === "sdk.dev.supermemory.ai"
)
}
export async function readJsonObject(
request: Request,
): Promise<Record<string, unknown>> {
const contentType = request.headers.get("content-type") ?? ""
if (!contentType.toLowerCase().startsWith("application/json")) {
throw new PlaygroundRequestError(
"Content-Type must be application/json",
415,
)
}
const contentLength = Number(request.headers.get("content-length"))
if (Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) {
throw new PlaygroundRequestError("Request body is too large", 413)
}
const raw = await request.text()
if (new TextEncoder().encode(raw).byteLength > MAX_BODY_BYTES) {
throw new PlaygroundRequestError("Request body is too large", 413)
}
let value: unknown
try {
value = JSON.parse(raw)
} catch {
throw new PlaygroundRequestError("Request body must be valid JSON", 400)
}
if (!isRecord(value)) {
throw new PlaygroundRequestError("Request body must be a JSON object", 400)
}
return value
}
export function parseMessages(value: unknown): ChatMessage[] {
if (!Array.isArray(value) || value.length === 0) {
throw new PlaygroundRequestError(
"At least one chat message is required",
400,
)
}
if (value.length > MAX_MESSAGES) {
throw new PlaygroundRequestError(
`A maximum of ${MAX_MESSAGES} messages is allowed`,
400,
)
}
let totalLength = 0
const messages = value.map((item, index): ChatMessage => {
if (!isRecord(item)) {
throw new PlaygroundRequestError(`Message ${index + 1} is invalid`, 400)
}
if (
item.role !== "user" &&
item.role !== "assistant" &&
item.role !== "system"
) {
throw new PlaygroundRequestError(
`Message ${index + 1} has an invalid role`,
400,
)
}
if (typeof item.content !== "string") {
throw new PlaygroundRequestError(
`Message ${index + 1} content must be text`,
400,
)
}
if (item.content.length > MAX_MESSAGE_LENGTH) {
throw new PlaygroundRequestError(
`Message ${index + 1} exceeds ${MAX_MESSAGE_LENGTH} characters`,
400,
)
}
totalLength += item.content.length
return { role: item.role, content: item.content }
})
if (totalLength > MAX_TOTAL_MESSAGE_LENGTH) {
throw new PlaygroundRequestError("Chat history is too large", 400)
}
if (
!messages.some(
(message) => message.role === "user" && message.content.trim().length > 0,
)
) {
throw new PlaygroundRequestError(
"Chat history must include a non-empty user message",
400,
)
}
return messages
}
export function parseIdentifier(
value: unknown,
name: string,
fallback?: string,
): string {
const resolved = typeof value === "string" ? value.trim() : fallback
if (!resolved) {
throw new PlaygroundRequestError(`${name} is required`, 400)
}
if (resolved.length > MAX_IDENTIFIER_LENGTH) {
throw new PlaygroundRequestError(
`${name} must be ${MAX_IDENTIFIER_LENGTH} characters or fewer`,
400,
)
}
return resolved
}
export function parseContainerTag(
value: unknown,
fallback = "sdk-playground",
): string {
const containerTag = parseIdentifier(value, "containerTag", fallback)
if (containerTag.length > MAX_CONTAINER_TAG_LENGTH) {
throw new PlaygroundRequestError(
`containerTag must be ${MAX_CONTAINER_TAG_LENGTH} characters or fewer`,
400,
)
}
if (!CONTAINER_TAG_PATTERN.test(containerTag)) {
throw new PlaygroundRequestError(
"containerTag may only contain letters, numbers, hyphens, underscores, and colons",
400,
)
}
return containerTag
}
export function parseConversationId(value: unknown): string {
const conversationId = parseIdentifier(value, "conversationId")
if (conversationId.length > MAX_CONVERSATION_ID_LENGTH) {
throw new PlaygroundRequestError(
`conversationId must be ${MAX_CONVERSATION_ID_LENGTH} characters or fewer`,
400,
)
}
return conversationId
}
export function parseOptionalText(
value: unknown,
name: string,
maxLength = MAX_MESSAGE_LENGTH,
): string | undefined {
if (value === undefined || value === null || value === "") return undefined
if (typeof value !== "string") {
throw new PlaygroundRequestError(`${name} must be text`, 400)
}
const resolved = value.trim()
if (!resolved) return undefined
if (resolved.length > maxLength) {
throw new PlaygroundRequestError(
`${name} must be ${maxLength} characters or fewer`,
400,
)
}
return resolved
}
export function parseMemoryMode(
value: unknown,
): "profile" | "query" | "full" | undefined {
if (value === undefined || value === null) return undefined
if (value === "profile" || value === "query" || value === "full") {
return value
}
throw new PlaygroundRequestError("Invalid memory mode", 400)
}
export function parseMiddlewareConfig(
value: unknown,
): Partial<MiddlewareRuntimeConfig> | undefined {
if (value === undefined || value === null) return undefined
if (!isRecord(value)) {
throw new PlaygroundRequestError("Invalid middleware configuration", 400)
}
if (
value.addMemory !== undefined &&
value.addMemory !== "always" &&
value.addMemory !== "never"
) {
throw new PlaygroundRequestError("Invalid addMemory value", 400)
}
for (const key of [
"verbose",
"includeToolCalls",
"skipMemoryOnError",
] as const) {
if (value[key] !== undefined && typeof value[key] !== "boolean") {
throw new PlaygroundRequestError(`Invalid ${key} value`, 400)
}
}
return {
...(value.addMemory !== undefined
? { addMemory: value.addMemory as "always" | "never" }
: {}),
...(value.verbose !== undefined
? { verbose: value.verbose as boolean }
: {}),
...(value.includeToolCalls !== undefined
? { includeToolCalls: value.includeToolCalls as boolean }
: {}),
...(value.skipMemoryOnError !== undefined
? { skipMemoryOnError: value.skipMemoryOnError as boolean }
: {}),
}
}
export function parseApiKeys(value: unknown): Partial<PlaygroundApiKeys> {
if (value === undefined || value === null) return {}
if (!isRecord(value)) {
throw new PlaygroundRequestError("Invalid API key configuration", 400)
}
return {
supermemoryApiKey: parseOptionalApiKey(
value.supermemoryApiKey,
"Supermemory API key",
),
openaiApiKey: parseOptionalApiKey(value.openaiApiKey, "OpenAI API key"),
}
}
function parseOptionalApiKey(value: unknown, name: string): string {
if (value === undefined || value === null || value === "") return ""
if (typeof value !== "string" || value.length > MAX_API_KEY_LENGTH) {
throw new PlaygroundRequestError(`${name} is invalid`, 400)
}
return value.trim()
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}

View file

@ -0,0 +1,116 @@
export type SdkLanguage = "typescript" | "python"
export type IntegrationMode = "middleware" | "tools" | "direct"
export interface ToolTraceEntry {
step: number
toolName: string
args: unknown
result?: unknown
}
export interface ChatSdkDefinition {
id: string
label: string
language: SdkLanguage
mode: IntegrationMode
package: string
description: string
available: boolean
}
export const CHAT_SDK_REGISTRY: ChatSdkDefinition[] = [
{
id: "ts-ai-sdk-middleware",
label: "AI SDK + middleware",
language: "typescript",
mode: "middleware",
package: "@supermemory/tools/ai-sdk",
description:
"withSupermemory wraps the model — auto-injects profile/search and saves conversations",
available: true,
},
{
id: "ts-openai-middleware",
label: "OpenAI SDK + middleware",
language: "typescript",
mode: "middleware",
package: "@supermemory/tools/openai",
description:
"withSupermemory on OpenAI client — same automatic memory path",
available: true,
},
{
id: "ts-ai-sdk-tools",
label: "AI SDK + tools",
language: "typescript",
mode: "tools",
package: "@supermemory/tools/ai-sdk",
description:
"Agent explicitly calls the 7 Supermemory tools via generateText",
available: true,
},
{
id: "ts-openai-tools",
label: "OpenAI SDK + tools",
language: "typescript",
mode: "tools",
package: "@supermemory/tools/openai",
description: "OpenAI function calling with the 7 Supermemory tools",
available: true,
},
{
id: "ts-ai-sdk-package",
label: "@supermemory/ai-sdk",
language: "typescript",
mode: "tools",
package: "@supermemory/ai-sdk",
description: "Re-export of tools/ai-sdk — same 7-tool agent",
available: true,
},
{
id: "py-openai-middleware",
label: "OpenAI + middleware",
language: "python",
mode: "middleware",
package: "supermemory-openai-sdk",
description:
"with_supermemory — automatic profile injection + conversation save",
available: true,
},
{
id: "py-openai-tools",
label: "OpenAI + tools",
language: "python",
mode: "tools",
package: "supermemory-openai-sdk",
description: "SupermemoryTools function-calling loop (7 tools)",
available: true,
},
{
id: "py-supermemory-direct",
label: "supermemory + manual context",
language: "python",
mode: "direct",
package: "supermemory",
description: "profile() then OpenAI — manual integration pattern from docs",
available: true,
},
]
export const PYTHON_SERVER_URL =
process.env.SDK_PLAYGROUND_PYTHON_URL ?? "http://127.0.0.1:8792"
export const TOOLS_SYSTEM_PROMPT = `You are a helpful assistant with Supermemory long-term memory.
You have tools to manage memory. Use them proactively:
- searchMemories: hybrid recall search before answering whenever user-specific context could help (do not wait to be asked)
- getProfile: broad static/dynamic user context at conversation start or when you need a wide overview
- addMemory: store a new generalizable fact
- documentList / documentAdd / documentDelete: manage source documents (documentDelete is permanent)
- memoryForget: soft-delete one profile fact by memoryId or exact content (not whole documents)
Before answering questions about the user, their preferences, or past context, search memories or get profile first. When the user asks you to remember something, use addMemory.`
export function getChatSdk(id: string): ChatSdkDefinition | undefined {
return CHAT_SDK_REGISTRY.find((s) => s.id === id)
}

View file

@ -0,0 +1,160 @@
import {
PARAMETER_DESCRIPTIONS,
TOOL_DESCRIPTIONS,
} from "../../../../packages/tools/src/tools-shared"
export interface CatalogParameter {
name: string
/** Omitted when this parameter is only exposed by the TypeScript tool schema. */
pythonName?: string
description: string
required?: boolean
}
export interface CatalogTool {
id: string
pythonName: string
description: string
parameters: CatalogParameter[]
}
export const TOOL_CATALOG: CatalogTool[] = [
{
id: "searchMemories",
pythonName: "search_memories",
description: TOOL_DESCRIPTIONS.searchMemories,
parameters: [
{
name: "informationToGet",
pythonName: "information_to_get",
description: PARAMETER_DESCRIPTIONS.informationToGet,
required: true,
},
{
name: "includeFullDocs",
description: PARAMETER_DESCRIPTIONS.includeFullDocs,
},
{
name: "limit",
pythonName: "limit",
description: PARAMETER_DESCRIPTIONS.limit,
},
],
},
{
id: "addMemory",
pythonName: "add_memory",
description: TOOL_DESCRIPTIONS.addMemory,
parameters: [
{
name: "memory",
pythonName: "memory",
description: PARAMETER_DESCRIPTIONS.memory,
required: true,
},
],
},
{
id: "getProfile",
pythonName: "get_profile",
description: TOOL_DESCRIPTIONS.getProfile,
parameters: [
{
name: "containerTag",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
{
name: "query",
pythonName: "query",
description: PARAMETER_DESCRIPTIONS.query,
},
],
},
{
id: "documentList",
pythonName: "document_list",
description: TOOL_DESCRIPTIONS.documentList,
parameters: [
{
name: "containerTag",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
{
name: "limit",
pythonName: "limit",
description: PARAMETER_DESCRIPTIONS.limit,
},
{
name: "page",
pythonName: "page",
description: PARAMETER_DESCRIPTIONS.page,
},
],
},
{
id: "documentDelete",
pythonName: "document_delete",
description: TOOL_DESCRIPTIONS.documentDelete,
parameters: [
{
name: "documentId",
pythonName: "document_id",
description: PARAMETER_DESCRIPTIONS.documentId,
required: true,
},
{
name: "containerTag",
description: PARAMETER_DESCRIPTIONS.documentContainerTag,
},
],
},
{
id: "documentAdd",
pythonName: "document_add",
description: TOOL_DESCRIPTIONS.documentAdd,
parameters: [
{
name: "content",
pythonName: "content",
description: PARAMETER_DESCRIPTIONS.content,
required: true,
},
{
name: "title",
pythonName: "title",
description: PARAMETER_DESCRIPTIONS.title,
},
{
name: "description",
pythonName: "description",
description: PARAMETER_DESCRIPTIONS.description,
},
],
},
{
id: "memoryForget",
pythonName: "memory_forget",
description: TOOL_DESCRIPTIONS.memoryForget,
parameters: [
{
name: "containerTag",
description: PARAMETER_DESCRIPTIONS.containerTag,
},
{
name: "memoryId",
pythonName: "memory_id",
description: PARAMETER_DESCRIPTIONS.memoryId,
},
{
name: "memoryContent",
pythonName: "memory_content",
description: PARAMETER_DESCRIPTIONS.memoryContent,
},
{
name: "reason",
pythonName: "reason",
description: PARAMETER_DESCRIPTIONS.reason,
},
],
},
]

View file

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}

108
bun.lock
View file

@ -142,6 +142,30 @@
"typescript": "^5",
},
},
"apps/sdk-playground": {
"name": "sdk-playground",
"version": "0.1.0",
"dependencies": {
"@ai-sdk/openai": "^2.0.22",
"@supermemory/ai-sdk": "workspace:*",
"@supermemory/tools": "workspace:*",
"ai": "^5.0.113",
"next": "16.0.7",
"openai": "^4.104.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"supermemory": "^4.25.4",
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.1.2",
"tailwindcss": "^4",
"typescript": "^5",
},
},
"apps/web": {
"name": "@repo/web",
"version": "0.1.0",
@ -261,7 +285,7 @@
"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",
},
@ -2580,6 +2604,8 @@
"concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="],
"concurrently": ["concurrently@9.2.4", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.9.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA=="],
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
@ -4448,6 +4474,8 @@
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
"sdk-playground": ["sdk-playground@workspace:apps/sdk-playground"],
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
@ -4484,7 +4512,7 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="],
"shell-quote": ["shell-quote@1.9.0", "", {}, "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA=="],
"shellwords": ["shellwords@0.1.1", "", {}, "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="],
@ -4628,7 +4656,7 @@
"supermemory-mcp": ["supermemory-mcp@workspace:apps/mcp"],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@ -5654,6 +5682,10 @@
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"concurrently/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
@ -5756,6 +5788,8 @@
"fx-runner/commander": ["commander@2.9.0", "", { "dependencies": { "graceful-readlink": ">= 1.0.0" } }, "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A=="],
"fx-runner/shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="],
"fx-runner/which": ["which@1.2.4", "", { "dependencies": { "is-absolute": "^0.1.7", "isexe": "^1.1.1" }, "bin": { "which": "./bin/which" } }, "sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA=="],
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
@ -5794,8 +5828,6 @@
"is-online/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
@ -5940,6 +5972,18 @@
"schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"sdk-playground/@types/node": ["@types/node@20.19.35", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ=="],
"sdk-playground/next": ["next@16.0.7", "", { "dependencies": { "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.7", "@next/swc-darwin-x64": "16.0.7", "@next/swc-linux-arm64-gnu": "16.0.7", "@next/swc-linux-arm64-musl": "16.0.7", "@next/swc-linux-x64-gnu": "16.0.7", "@next/swc-linux-x64-musl": "16.0.7", "@next/swc-win32-arm64-msvc": "16.0.7", "@next/swc-win32-x64-msvc": "16.0.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A=="],
"sdk-playground/react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"sdk-playground/react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"sdk-playground/supermemory": ["supermemory@4.25.4", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-97ME3rlmu7OmsXJTb9OgXOD+3VUv4Wej0ZX9xezG+LKkMwrzi4xeeAZaOJFcr0oI/QQjcHG2WOzm+und1e7MFA=="],
"sdk-playground/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"send/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
@ -6590,6 +6634,8 @@
"@sap-cloud-sdk/util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@sap-cloud-sdk/util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"@sentry/bundler-plugin-core/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
@ -6680,6 +6726,16 @@
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"concurrently/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"concurrently/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"concurrently/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"concurrently/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
@ -6706,6 +6762,8 @@
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@ -6754,6 +6812,8 @@
"find-process/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"find-process/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"front-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"fx-runner/which/isexe": ["isexe@1.1.2", "", {}, "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw=="],
@ -6824,6 +6884,28 @@
"raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"sdk-playground/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"sdk-playground/next/@next/env": ["@next/env@16.0.7", "", {}, "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw=="],
"sdk-playground/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg=="],
"sdk-playground/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA=="],
"sdk-playground/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww=="],
"sdk-playground/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g=="],
"sdk-playground/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA=="],
"sdk-playground/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w=="],
"sdk-playground/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q=="],
"sdk-playground/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug=="],
"sdk-playground/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@ -7336,6 +7418,14 @@
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"concurrently/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"concurrently/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"concurrently/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"concurrently/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"docs-test/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"docs-test/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@ -7348,6 +7438,8 @@
"memory-graph-playground/next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"sdk-playground/next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"supermemory-mcp/agents/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"supermemory-mcp/agents/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
@ -7502,6 +7594,12 @@
"agents/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"concurrently/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"concurrently/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"concurrently/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@mastra/core/@modelcontextprotocol/sdk/express/body-parser/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"@mintlify/link-rot/@mintlify/scraping/@mintlify/common/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],

View file

@ -10,6 +10,11 @@
"apps/memory-graph-playground": {
"name": "graph.dev.supermemory",
"script": "dev:app"
},
"apps/sdk-playground": {
"name": "sdk.dev.supermemory",
"script": "dev:app",
"appPort": 3005
}
}
}

View file

@ -5,7 +5,7 @@
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**"]
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]