merge conflicts

This commit is contained in:
Sreeram Sreedhar 2026-04-22 17:41:51 -07:00
commit 92c7163608
16 changed files with 302 additions and 47 deletions

View file

@ -16,7 +16,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.4
bun-version: 1.3.6
- name: Install dependencies
run: bun install --frozen-lockfile

View file

@ -31,9 +31,6 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: bun install

View file

@ -31,9 +31,6 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: bun install

View file

@ -31,9 +31,6 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: bun install

View file

@ -106,6 +106,18 @@ const model = withSupermemory(openai("gpt-4"), "user-123", {
// Console output shows memory retrieval details
```
### When Supermemory errors (optional: continue without memories)
If the Supermemory API returns an error (or is unreachable), memory retrieval fails before the LLM runs. By default that error **propagates** (fails the call).
To continue the LLM request **without** injected memories instead, opt in with `skipMemoryOnError: true`. Use `verbose: true` if you want console output when that happens.
```typescript
const model = withSupermemory(openai("gpt-5"), "user-123", {
skipMemoryOnError: true
})
```
---
## Memory Tools

View file

@ -24,9 +24,12 @@ type Props = {
name?: string
}
const CONTAINER_TAGS_TTL_MS = 5 * 60 * 1000
export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
private clientInfo: { name: string; version?: string } | null = null
private cachedContainerTags: string[] = []
private containerTagsLastFetchedAt: number | null = null
server = new McpServer({
name: "supermemory",
@ -168,8 +171,8 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
"supermemory://projects",
{},
async () => {
const client = this.getClient()
const projects = await client.getProjects()
await this.ensureContainerTagsFresh()
const projects = this.cachedContainerTags
return {
contents: [
@ -193,15 +196,19 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
refresh: z
.boolean()
.optional()
.default(true)
.describe("Refresh the list from the server (default: true)"),
.default(false)
.describe(
"Force refresh from the server (default: false; uses cache with TTL)",
),
}),
},
// @ts-expect-error - zod type inference issue with MCP SDK
async (args: { refresh?: boolean }) => {
try {
if (args.refresh !== false) {
if (args.refresh === true) {
await this.refreshContainerTags()
} else {
await this.ensureContainerTagsFresh()
}
const projects = this.cachedContainerTags
@ -566,6 +573,10 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
const result = await client.createMemory(content)
if (!this.cachedContainerTags.includes(result.containerTag)) {
await this.refreshContainerTags()
}
// Track memory added event
posthog
.memoryAdded({
@ -758,10 +769,21 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
return this.ctx.id.name || "unknown"
}
private async ensureContainerTagsFresh(): Promise<void> {
const now = Date.now()
const needsRefresh =
this.containerTagsLastFetchedAt === null ||
now - this.containerTagsLastFetchedAt > CONTAINER_TAGS_TTL_MS
if (needsRefresh) {
await this.refreshContainerTags()
}
}
private async refreshContainerTags(): Promise<void> {
try {
const client = this.getClient()
this.cachedContainerTags = await client.getProjects()
this.containerTagsLastFetchedAt = Date.now()
} catch (error) {
console.error("Failed to fetch container tags:", error)
}

View file

@ -13,7 +13,9 @@
"resolveJsonModule": true,
"outDir": "dist",
"rootDir": "src",
"baseUrl": "."
"paths": {
"*": ["./*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules"]

View file

@ -2,12 +2,14 @@
import { EnsureWorkspace } from "@/components/ensure-workspace"
import { MobileBanner } from "@/components/mobile-banner"
import { NextAppResearchCta } from "@/components/next-app-research-cta"
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<>
<MobileBanner />
<EnsureWorkspace>{children}</EnsureWorkspace>
<NextAppResearchCta />
</>
)
}

View file

@ -11,6 +11,7 @@ import { Suspense } from "react"
import { Toaster } from "@ui/components/sonner"
import { NuqsAdapter } from "nuqs/adapters/next/app"
import { ThemeProvider } from "@/lib/theme-provider"
import Script from "next/script"
const font = Space_Grotesk({
subsets: ["latin"],
@ -69,6 +70,11 @@ export default function RootLayout({
</QueryProvider>
</AutumnProvider>
</ThemeProvider>
<Script
src="https://lobbyside.com/widget.js"
data-widget-id="e385c52f-4dd3-4fb2-81eb-da3a78059014"
strategy="lazyOnload"
/>
</body>
</html>
)

View file

@ -71,10 +71,12 @@ export function MemoryGraph({
maxNodes={maxNodes}
canvasRef={canvasRef}
totalCount={totalCount}
colors={{
bg: "transparent",
edgeDerives: "#9ca3af",
} as any}
colors={
{
bg: "transparent",
edgeDerives: "#9ca3af",
} as any
}
{...rest}
>
{children}

View file

@ -0,0 +1,144 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import { usePathname } from "next/navigation"
import { Phone, Users, X as XIcon } from "lucide-react"
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { analytics } from "@/lib/analytics"
const STORAGE_KEY = "sm_next_app_research_cta_dismissed_v1"
const BOOK_CALL_HREF = "https://cal.com/supermemory/growth"
function ResearchCtaHeroGraphic() {
return (
<div
id="next-app-research-cta-hero"
className={cn(
"relative flex min-h-[4.5rem] w-full shrink-0 items-center justify-center overflow-hidden rounded-xl py-5",
"border border-white/[0.1] bg-gradient-to-b from-[#141c28] to-[#0D121A]",
)}
aria-hidden
>
<div
className="pointer-events-none absolute inset-0 opacity-[0.45]"
style={{
background:
"radial-gradient(ellipse 85% 90% at 50% 30%, rgba(59, 130, 246, 0.2), transparent 65%)",
}}
/>
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div
className="h-[120%] w-px bg-gradient-to-b from-transparent via-[#5B8DEF]/35 to-transparent"
style={{ transform: "rotate(52deg)" }}
/>
<div
className="absolute h-[120%] w-px bg-gradient-to-b from-transparent via-[#9B7AFF]/30 to-transparent"
style={{ transform: "rotate(-52deg)" }}
/>
</div>
<div className="relative z-10 flex flex-row items-center justify-center gap-10 px-3">
<Phone className="size-[24px] text-[#7EB0FF]" strokeWidth={1.65} />
<span
className={cn(
dmSans125ClassName(),
"select-none text-[17px] font-light leading-none text-[#6B9FFF]/75",
)}
>
×
</span>
<Users className="size-[24px] text-[#B49CFB]" strokeWidth={1.65} />
</div>
</div>
)
}
export function NextAppResearchCta() {
const pathname = usePathname()
const [mounted, setMounted] = useState(false)
const [dismissed, setDismissed] = useState(false)
useEffect(() => {
setMounted(true)
setDismissed(localStorage.getItem(STORAGE_KEY) === "1")
}, [])
const handleDismiss = useCallback(() => {
localStorage.setItem(STORAGE_KEY, "1")
setDismissed(true)
analytics.nextAppResearchCtaDismissed()
}, [])
const handleBookClick = useCallback(() => {
analytics.nextAppResearchCtaBookCallClicked()
}, [])
if (!mounted || dismissed || pathname.startsWith("/onboarding")) {
return null
}
return (
<section
id="next-app-research-cta"
className={cn(
"fixed z-[45] bottom-4 left-4 max-w-[min(calc(100vw-2rem),19rem)]",
"rounded-xl border border-white/[0.08] bg-[#0D121A]/95 backdrop-blur-md",
"shadow-[0_8px_32px_rgba(0,0,0,0.35)] p-3.5",
)}
aria-label="Research participant invitation"
>
<div className="flex flex-col gap-3">
<ResearchCtaHeroGraphic />
<div className="min-w-0 w-full">
<div className="flex items-start gap-1">
<p
className={cn(
dmSans125ClassName(),
"flex-1 min-w-0 font-medium text-[12px] text-[#FAFAFA] tracking-[-0.12px]",
)}
>
Be part of the next supermemory app
</p>
<button
type="button"
onClick={handleDismiss}
className={cn(
"shrink-0 rounded-md p-1 -mr-1 -mt-0.5",
"text-muted-foreground hover:text-foreground transition-colors",
"cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
aria-label="Dismiss"
>
<XIcon className="size-4" />
</button>
</div>
<p
className={cn(
dmSans125ClassName(),
"mt-1 text-[12px] text-[#737373] tracking-[-0.12px]",
)}
>
Share what you want next—we’d love a quick call.
</p>
<div className="mt-2.5 flex justify-end">
<a
href={BOOK_CALL_HREF}
onClick={handleBookClick}
target="_blank"
rel="noopener noreferrer"
className={cn(
dmSans125ClassName(),
"inline-flex text-[13px] font-medium text-[#A3A3A3]",
"tracking-[-0.13px] underline underline-offset-4 decoration-white/20",
"hover:text-[#FAFAFA] hover:decoration-white/40 transition-colors",
)}
>
Book a call
</a>
</div>
</div>
</div>
</section>
)
}

View file

@ -46,6 +46,11 @@ export const analytics = {
connectionAuthCompleted: () => safeCapture("connection_auth_completed"),
connectionAuthFailed: () => safeCapture("connection_auth_failed"),
nextAppResearchCtaDismissed: () =>
safeCapture("next_app_research_cta_dismissed"),
nextAppResearchCtaBookCallClicked: () =>
safeCapture("next_app_research_cta_book_call_clicked"),
mcpViewOpened: () => safeCapture("mcp_view_opened"),
mcpInstallCmdCopied: () => safeCapture("mcp_install_cmd_copied"),

View file

@ -12,7 +12,7 @@
"engines": {
"node": ">=20"
},
"packageManager": "bun@1.3.4",
"packageManager": "bun@1.3.6",
"workspaces": [
"apps/*",
"!apps/raycast-extension",

View file

@ -197,7 +197,11 @@ export function MemoryGraph({
setViewportVersion((v) => v + 1)
}
const { hasMore: more, isLoadingMore: loading, onLoadMore: load } = loadMoreRef.current
const {
hasMore: more,
isLoadingMore: loading,
onLoadMore: load,
} = loadMoreRef.current
if (!more || loading || !load || !viewportRef.current) return
const vp = viewportRef.current
@ -205,14 +209,17 @@ export function MemoryGraph({
if (currentNodes.length === 0) return
const topLeft = vp.screenToWorld(0, 0)
const bottomRight = vp.screenToWorld(containerSize.width, containerSize.height)
const bottomRight = vp.screenToWorld(
containerSize.width,
containerSize.height,
)
const viewW = bottomRight.x - topLeft.x
const viewH = bottomRight.y - topLeft.y
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
let minX = Number.POSITIVE_INFINITY
let minY = Number.POSITIVE_INFINITY
let maxX = Number.NEGATIVE_INFINITY
let maxY = Number.NEGATIVE_INFINITY
for (const n of currentNodes) {
if (n.x < minX) minX = n.x
if (n.y < minY) minY = n.y
@ -613,7 +620,6 @@ export function MemoryGraph({
colors={colors}
/>
{!isLoading && !nodes.some((n) => n.type === "document") && children && (
<div style={emptyStateStyle}>{children}</div>
)}

View file

@ -50,6 +50,12 @@ interface WrapVercelLanguageModelOptions {
* ```
*/
promptTemplate?: PromptTemplate
/**
* When Supermemory memory retrieval / injection fails:
* - `false` (default): propagate the error.
* - `true`: log and call the base model with the original prompt (no memories).
*/
skipMemoryOnError?: boolean
}
/**
@ -72,6 +78,7 @@ interface WrapVercelLanguageModelOptions {
* @param options.addMemory - Optional mode for memory search: "always", "never" (default: "never")
* @param options.apiKey - Optional Supermemory API key to use instead of the environment variable
* @param options.baseUrl - Optional base URL for the Supermemory API (default: "https://api.supermemory.ai")
* @param options.skipMemoryOnError - When memory retrieval fails: `false` (default) throws; `true` continues without injected memories
*
* @returns A wrapped language model that automatically includes relevant memories in prompts
*
@ -93,7 +100,7 @@ interface WrapVercelLanguageModelOptions {
* ```
*
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
* @throws {Error} When supermemory API request fails
* @throws {Error} When supermemory memory retrieval fails unless `skipMemoryOnError` is `true`
*/
const wrapVercelLanguageModel = <T extends LanguageModel>(
model: T,
@ -119,19 +126,42 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
promptTemplate: options?.promptTemplate,
})
const skipMemoryOnError = options?.skipMemoryOnError ?? false
// Proxy keeps prototype/getter fields (e.g. provider, modelId) that `{ ...model }` drops.
return new Proxy(model, {
get(target, prop, receiver) {
if (prop === "doGenerate") {
return async (params: LanguageModelCallOptions) => {
let modelParams: LanguageModelCallOptions = params
try {
const transformedParams = await transformParamsWithMemory(
params,
ctx,
)
modelParams = await transformParamsWithMemory(params, ctx)
} catch (memoryError) {
if (skipMemoryOnError) {
ctx.logger.warn(
"Supermemory retrieval failed; continuing without injected memories",
{
error:
memoryError instanceof Error
? memoryError.message
: "Unknown error",
},
)
modelParams = params
} else {
ctx.logger.error("Error during memory retrieval for generation", {
error:
memoryError instanceof Error
? memoryError.message
: "Unknown error",
})
throw memoryError
}
}
try {
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
const result = await target.doGenerate(transformedParams as any)
const result = await target.doGenerate(modelParams as any)
const userMessage = getLastUserMessage(params)
if (
@ -168,15 +198,36 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
return async (params: LanguageModelCallOptions) => {
let generatedText = ""
let modelParams: LanguageModelCallOptions = params
try {
const transformedParams = await transformParamsWithMemory(
params,
ctx,
)
modelParams = await transformParamsWithMemory(params, ctx)
} catch (memoryError) {
if (skipMemoryOnError) {
ctx.logger.warn(
"Supermemory retrieval failed; continuing without injected memories",
{
error:
memoryError instanceof Error
? memoryError.message
: "Unknown error",
},
)
modelParams = params
} else {
ctx.logger.error("Error during memory retrieval for stream", {
error:
memoryError instanceof Error
? memoryError.message
: "Unknown error",
})
throw memoryError
}
}
try {
const { stream, ...rest } = await target.doStream(
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
transformedParams as any,
modelParams as any,
)
const transformStream = new TransformStream<

View file

@ -119,10 +119,16 @@ export async function realClaudeMemoryExample() {
const toolResults = []
if (responseData.content) {
const memoryToolCalls = responseData.content.filter(
(block: any): block is { type: 'tool_use'; id: string; name: 'memory'; input: { command: MemoryCommand; path: string } } =>
block.type === "tool_use" && block.name === "memory",
)
const memoryToolCalls = responseData.content.filter(
(
block: any,
): block is {
type: "tool_use"
id: string
name: "memory"
input: { command: MemoryCommand; path: string }
} => block.type === "tool_use" && block.name === "memory",
)
const results = await Promise.all(
memoryToolCalls.map((block: any) => {
@ -196,10 +202,16 @@ export async function processClaudeResponse(
const toolResults = []
if (claudeResponseData.content) {
const memoryToolCalls = claudeResponseData.content.filter(
(block: any): block is { type: 'tool_use'; id: string; name: 'memory'; input: { command: MemoryCommand; path: string } } =>
block.type === "tool_use" && block.name === "memory",
)
const memoryToolCalls = claudeResponseData.content.filter(
(
block: any,
): block is {
type: "tool_use"
id: string
name: "memory"
input: { command: MemoryCommand; path: string }
} => block.type === "tool_use" && block.name === "memory",
)
const results = await Promise.all(
memoryToolCalls.map((block: any) =>