Skip for now
@@ -308,7 +308,7 @@ export function StepTeam({
variant="insideOut"
onClick={onContinue}
disabled={submitting}
- className="rounded-full px-5 py-[10px] text-[13px] font-medium text-[#fafafa]"
+ className="rounded-full px-4 py-2 text-[13px] font-medium text-[#fafafa] md:px-5 md:py-[10px]"
>
{submitting ? (
<>
diff --git a/apps/web/components/settings/company-brain-automations.tsx b/apps/web/components/settings/company-brain-automations.tsx
index 26d1fda3..74f8e7b0 100644
--- a/apps/web/components/settings/company-brain-automations.tsx
+++ b/apps/web/components/settings/company-brain-automations.tsx
@@ -1,6 +1,5 @@
"use client"
-import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import { cn } from "@lib/utils"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
@@ -9,26 +8,19 @@ import {
CalendarClock,
ChevronDown,
ChevronUp,
- FilePlus2,
FileText,
GitPullRequest,
Info,
LifeBuoy,
ListTodo,
Loader2,
- Lock,
MessageCircleQuestion,
Plus,
+ Radar,
Trash2,
} from "lucide-react"
import { useRef, useState } from "react"
import { toast } from "sonner"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@ui/components/dropdown-menu"
import {
Select,
SelectContent,
@@ -167,7 +159,7 @@ const PRESETS: Preset[] = [
{
id: "standup",
category: "team",
- label: "Daily standup",
+ label: "Morning checkup",
description:
"Shipped work, decisions, blockers & open questions from the last 24h.",
icon: CalendarClock,
@@ -179,7 +171,7 @@ const PRESETS: Preset[] = [
{
id: "weekly-recap",
category: "team",
- label: "Weekly team recap",
+ label: "Company progress",
description:
"Decisions, shipped work & unresolved threads from the past week.",
icon: CalendarClock,
@@ -236,6 +228,18 @@ const PRESETS: Preset[] = [
frequency: "daily",
time: "09:00",
},
+ {
+ id: "competitor-check",
+ category: "product",
+ label: "Competitor check",
+ description: "What competitors shipped, announced, or changed this week.",
+ icon: Radar,
+ prompt:
+ "Check what our competitors shipped, announced, or changed recently — launches, pricing changes, and anything the team should react to.",
+ frequency: "weekly",
+ weekday: 1,
+ time: "09:00",
+ },
{
id: "release-notes",
category: "product",
@@ -273,38 +277,12 @@ function sortPresets(connected: Set
): Preset[] {
return [...PRESETS].sort((a, b) => rank(a) - rank(b))
}
-// Up to 3 presets spanning distinct categories (connection-preferred order).
-function galleryPresets(sorted: Preset[]): Preset[] {
- const seen = new Set()
- const out: Preset[] = []
- for (const p of sorted) {
- if (seen.has(p.category)) continue
- seen.add(p.category)
- out.push(p)
- if (out.length === 3) break
- }
- return out
-}
-
function cadenceLabel(p: Preset): string {
if (p.frequency === "weekly")
return `Weekly · ${WEEKDAYS[p.weekday ?? 1] ?? "Mon"} ${p.time}`
return `Daily · ${p.time}`
}
-function SectionTitle({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- )
-}
-
const controlClass = cn(
dmSans125ClassName(),
"h-9 w-full rounded-[10px] border border-white/[0.08] bg-[#0D0F14] px-3 text-[13px] text-[#FAFAFA] outline-none disabled:opacity-50",
@@ -318,9 +296,6 @@ const selectItemClass =
"cursor-pointer rounded-[8px] text-[13px] text-[#FAFAFA] hover:bg-white/10 hover:text-white data-[highlighted]:bg-white/10 data-[highlighted]:text-white focus:bg-white/10 focus:text-white"
const DM_VALUE = "__dm__"
-const menuItemClass =
- "cursor-pointer gap-2.5 rounded-lg px-2.5 py-2 text-[13px] font-medium text-white/85 hover:bg-white/[0.06] hover:text-white data-[highlighted]:bg-white/[0.06] data-[highlighted]:text-white focus:bg-white/[0.06] focus:text-white"
-
function AutomationCard({
initial,
id,
@@ -830,7 +805,7 @@ function AutomationRow({
)
}
-function PresetTile({
+function PresetCard({
preset,
onPick,
}: {
@@ -844,19 +819,31 @@ function PresetTile({
onClick={onPick}
className={cn(
dmSans125ClassName(),
- "flex items-center gap-3 rounded-[10px] border border-white/[0.06] bg-[#14161A] px-3 py-2.5 text-left transition-colors hover:border-white/[0.12] hover:bg-[#171A1F]",
+ "flex min-w-0 cursor-pointer flex-col justify-between gap-3 rounded-xl bg-[#14161A] p-4 text-left",
+ "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)] transition-colors hover:bg-[#171A1F]",
)}
- title={preset.description}
>
-
-
-
- {preset.label}
-
-
+
+
+
+
+
+
+ {preset.label}
+
+
+ {preset.description}
+
+
+
+
+
{cadenceLabel(preset)}
-
+
+ Use template
+
+
)
}
@@ -873,17 +860,6 @@ export default function CompanyBrainAutomations() {
const removeDraft = (key: number) =>
setDrafts((d) => d.filter((x) => x.key !== key))
- const roleQuery = useQuery({
- queryKey: ["company-brain-automations", "role"],
- queryFn: async () =>
- (await authClient.organization.getActiveMember()).data?.role ?? null,
- staleTime: 60_000,
- enabled: isCompanyBrain,
- })
- const isAdmin = ["owner", "admin"].includes(
- (roleQuery.data ?? "").toLowerCase(),
- )
-
const listQuery = useQuery({
queryKey: ["company-brain-automations", "list", org?.id],
queryFn: async () => {
@@ -935,71 +911,12 @@ export default function CompanyBrainAutomations() {
queryKey: ["company-brain-automations", "list"],
})
}
- const showGallery = automations.length === 0 && drafts.length === 0
+ const usedTitles = new Set(automations.map((a) => a.title))
+ const availablePresets = presets.filter((p) => !usedTitles.has(p.label))
+ const hasList = automations.length > 0 || drafts.length > 0
return (
-
-
-
Channel automations
-
-
-
-
- New automation
-
-
-
- addDraft(emptyDraft())}
- className={menuItemClass}
- >
-
- Blank automation
-
- {presets.map((p) => {
- const Icon = p.icon
- return (
- addDraft(presetToDraft(p))}
- className={menuItemClass}
- >
-
- {p.label}
-
- )
- })}
-
-
-
-
-
-
- Read-only scheduled summaries posted to a channel.{" "}
- {isAdmin
- ? "You manage all across the org."
- : "You manage the ones you create."}
-
-
-
{automations.map((a) =>
openId === a.id ? (
@@ -1044,40 +961,37 @@ export default function CompanyBrainAutomations() {
/>
))}
- {showGallery ? (
-
-
- Start from a template:
-
-
- {galleryPresets(presets).map((p) => (
-
addDraft(presetToDraft(p))}
- />
- ))}
- addDraft(emptyDraft())}
- className={cn(
- dmSans125ClassName(),
- "flex items-center justify-center gap-2 rounded-[10px] border border-dashed border-white/[0.1] bg-transparent px-3 py-2.5 text-[13px] text-[#9A9A9A] transition-colors hover:border-white/20 hover:text-[#FAFAFA]",
- )}
- >
-
- Start from scratch
-
-
-
- More templates in the New automation menu.
-
-
+ {hasList ? (
+
+ Templates
+
) : null}
+
+ {availablePresets.map((p) => (
+
addDraft(presetToDraft(p))}
+ />
+ ))}
+ addDraft(emptyDraft())}
+ className={cn(
+ dmSans125ClassName(),
+ "flex min-h-[104px] cursor-pointer items-center justify-center gap-2 rounded-xl border border-[#2A313C] border-dashed",
+ "text-[13px] font-medium text-[#737B87] transition-colors hover:border-[#3A4150] hover:text-[#FAFAFA]",
+ )}
+ >
+
+ New automation
+
+
)
diff --git a/apps/web/components/settings/company-brain-connections.tsx b/apps/web/components/settings/company-brain-connections.tsx
index a6fb1ff8..ae1489a4 100644
--- a/apps/web/components/settings/company-brain-connections.tsx
+++ b/apps/web/components/settings/company-brain-connections.tsx
@@ -1,10 +1,22 @@
"use client"
-import { authClient } from "@lib/auth"
+import { useOrgMemberRole } from "@/hooks/use-org-member-role"
import { cn } from "@lib/utils"
-import { useQuery } from "@tanstack/react-query"
-import { Loader2, Lock } from "lucide-react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { ChevronDown, Loader2, Plus, XIcon } from "lucide-react"
import { useCallback, useEffect, useState } from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@ui/components/dialog"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@ui/components/dropdown-menu"
import { toast } from "sonner"
import { dmSans125ClassName } from "@/lib/fonts"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
@@ -33,7 +45,6 @@ type ConnRow = {
userId: string | null
}
type SlackStatus = { connected: boolean; teamName: string | null }
-type Scope = "org" | "user"
function titleCase(s: string) {
return s.replace(/\b\w/g, (c) => c.toUpperCase())
@@ -48,28 +59,20 @@ function slugifyMcpName(value: string) {
.slice(0, 63)
}
-function SecondaryButton({
- children,
- href,
-}: {
- children: React.ReactNode
- href: string
-}) {
- return (
-
- {children}
-
- )
-}
+const pillLinkClass = cn(
+ "relative flex h-8 min-w-[94px] shrink-0 items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 sm:h-9 sm:min-w-[116px] sm:px-5",
+ "text-[12px] font-medium text-[#FAFAFA] sm:text-[14px]",
+ "shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
+ "cursor-pointer transition-opacity hover:opacity-80",
+)
-function StatusDot({ connected }: { connected: boolean }) {
+function ScopeChip({
+ label,
+ connected,
+}: {
+ label: string
+ connected: boolean
+}) {
return (
- {connected ? "Connected" : "Not connected"}
+ {label}
)
}
+const menuItemClass =
+ "gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-white/85 hover:bg-white/[0.06] focus:bg-white/[0.06] focus:text-white cursor-pointer"
+
function AppCard({
name,
subtitle,
icon,
- connected,
- canConnect,
- canDisconnect,
- lockedHint,
+ userConnected,
+ orgConnected,
+ isAdmin,
+ personalOnly,
busy,
onConnect,
onDisconnect,
@@ -104,16 +110,20 @@ function AppCard({
name: string
subtitle: string
icon: React.ReactNode
- connected: boolean
- canConnect: boolean
- canDisconnect: boolean
- lockedHint?: string
+ userConnected: boolean
+ orgConnected: boolean
+ isAdmin: boolean
+ personalOnly?: boolean
busy: boolean
- onConnect: () => void
- onDisconnect: () => void
+ onConnect: (shared: boolean) => void
+ onDisconnect: (shared: boolean) => void
}) {
+ const anyConnected = userConnected || orgConnected
+ const showOrgChip = !personalOnly && (orgConnected || isAdmin)
+ const adminMenu = isAdmin && !personalOnly
+
return (
-
+
{icon}
@@ -138,67 +148,154 @@ function AppCard({
-
- {connected && canDisconnect ? (
-
+
+ {personalOnly || !anyConnected ? (
+
+ ) : (
+ <>
+
+ {showOrgChip ? (
+
+ ) : null}
+ >
+ )}
+
+ {adminMenu ? (
+
+
+
+ {busy ? (
+
+ ) : (
+ <>
+ {anyConnected ? "Manage" : "Connect"}
+
+ >
+ )}
+
+
+
+
+ userConnected ? onDisconnect(false) : onConnect(false)
+ }
+ >
+ {userConnected ? "Disconnect my account" : "Connect my account"}
+
+
+ orgConnected ? onDisconnect(true) : onConnect(true)
+ }
+ >
+ {orgConnected
+ ? "Disconnect workspace"
+ : "Connect for workspace"}
+
+
+
+ ) : userConnected ? (
+ onDisconnect(false)} disabled={busy}>
{busy && }
Disconnect
- ) : (
- !connected &&
- (canConnect ? (
-
- {busy && }
- Connect
-
- ) : lockedHint ? (
-
-
- {lockedHint}
-
- ) : null)
+ ) : personalOnly ? null : (
+ onConnect(false)} disabled={busy}>
+ {busy && }
+ Connect
+
)}
)
}
-function ScopeToggle({
- scope,
- onChange,
+function SlackCard({
+ status,
+ isAdmin,
+ installHref,
}: {
- scope: Scope
- onChange: (s: Scope) => void
+ status: SlackStatus | null
+ isAdmin: boolean
+ installHref: string
}) {
- const items: { id: Scope; label: string }[] = [
- { id: "org", label: "Organization" },
- { id: "user", label: "Personal" },
- ]
+ const connected = status?.connected ?? false
return (
-
- {items.map((it) => (
-
onChange(it.id)}
- className={cn(
- dmSans125ClassName(),
- "rounded-full px-4 h-8 text-[13px] font-medium transition-colors",
- scope === it.id
- ? "bg-[#1E293B] text-[#FAFAFA]"
- : "text-[#737373] hover:text-[#FAFAFA]",
- )}
- >
- {it.label}
-
- ))}
+
+
+
+
+
+
+
+ Slack
+
+
+ Messaging
+
+
+
+
)
}
function RowSkeleton() {
return (
-
+
@@ -206,7 +303,7 @@ function RowSkeleton() {
-
+
)
}
@@ -219,19 +316,11 @@ export default function CompanyBrainConnections() {
const [rows, setRows] = useState
([])
const [slackStatus, setSlackStatus] = useState(null)
const [busy, setBusy] = useState(null)
- const [scope, setScope] = useState("user")
+ const [customOpen, setCustomOpen] = useState(false)
const [customName, setCustomName] = useState("")
const [customServerUrl, setCustomServerUrl] = useState("")
- const roleQuery = useQuery({
- queryKey: ["company-brain-connections", "role"],
- queryFn: async () =>
- (await authClient.organization.getActiveMember()).data?.role ?? null,
- staleTime: 60_000,
- enabled: isCompanyBrain,
- })
- const role = (roleQuery.data ?? "").toLowerCase()
- const isAdmin = role === "owner" || role === "admin"
+ const { isAdmin } = useOrgMemberRole(isCompanyBrain)
const load = useCallback(async () => {
const [catRes, connRes, slackRes] = await Promise.all([
@@ -388,8 +477,12 @@ export default function CompanyBrainConnections() {
}
if (data.authUrl) {
window.open(data.authUrl, "_blank", "noopener")
+ setCustomOpen(false)
+ setCustomName("")
+ setCustomServerUrl("")
} else if (data.ok) {
toast.success(`${slug} connected.`)
+ setCustomOpen(false)
setCustomName("")
setCustomServerUrl("")
await load()
@@ -461,43 +554,9 @@ export default function CompanyBrainConnections() {
!catalogSlugs.has(row.serverSlug),
)
: []
- const shared = scope === "org"
- const description = shared
- ? "Connected by admins. Used for reads when you haven't connected your own."
- : "Your personal accounts, used for your actions and your reads."
-
return (
-
-
- {slackStatus?.connected && slackStatus.teamName ? (
-
- Slack · {slackStatus.teamName}
-
- ) : null}
- {isAdmin ? (
-
-
- Reconnect Slack
-
- ) : null}
-
-
-
- {description}
-
-
-
+
{loading ? (
<>
@@ -506,100 +565,128 @@ export default function CompanyBrainConnections() {
>
) : (
<>
- {!shared && isStaff ? (
-
- ) : null}
-
+
{apps.map((entry) => (
connect(entry, shared)}
- onDisconnect={() => disconnect(entry, shared)}
+ userConnected={isConnected(entry.slug, false)}
+ orgConnected={isConnected(entry.slug, true)}
+ isAdmin={isAdmin}
+ busy={busy?.startsWith(`${entry.slug}:`) ?? false}
+ onConnect={(shared) => connect(entry, shared)}
+ onDisconnect={(shared) => disconnect(entry, shared)}
/>
))}
- {!shared &&
- customRows.map((row) => (
- {}}
- onDisconnect={() =>
- disconnect(
- {
- slug: row.serverSlug,
- name: titleCase(row.serverSlug.replace(/-/g, " ")),
- category: "Custom OAuth MCP",
- authType: "oauth",
- },
- false,
- )
- }
- />
- ))}
+ {customRows.map((row) => (
+ {}}
+ onDisconnect={() =>
+ disconnect(
+ {
+ slug: row.serverSlug,
+ name: titleCase(row.serverSlug.replace(/-/g, " ")),
+ category: "Custom OAuth MCP",
+ authType: "oauth",
+ },
+ false,
+ )
+ }
+ />
+ ))}
+ {isStaff ? (
+ setCustomOpen(true)}
+ className={cn(
+ dmSans125ClassName(),
+ "flex min-h-[104px] cursor-pointer items-center justify-center gap-2 rounded-xl border border-[#2A313C] border-dashed",
+ "text-[13px] font-medium text-[#737B87] transition-colors hover:border-[#3A4150] hover:text-[#FAFAFA]",
+ )}
+ >
+
+ Add custom MCP
+
+ ) : null}
>
)}
+
+
+
+
+
+
+ Custom MCP server
+
+
+ Add a personal OAuth MCP server by URL.
+
+
+
+
+ Close
+
+
+
+
+
+
)
}
diff --git a/apps/web/components/settings/company-brain-models.tsx b/apps/web/components/settings/company-brain-models.tsx
new file mode 100644
index 00000000..37815ef2
--- /dev/null
+++ b/apps/web/components/settings/company-brain-models.tsx
@@ -0,0 +1,537 @@
+"use client"
+
+import { cn } from "@lib/utils"
+import { Check, ChevronDown, Loader2, Lock } from "lucide-react"
+import { useMemo, useState } from "react"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@ui/components/select"
+import {
+ type BrainModelConfig,
+ type BrainModelRole,
+ type BrainReasoningEffort,
+ type BrainReasoningKey,
+ useBrainModels,
+ useUpdateBrainModels,
+} from "@/hooks/use-brain-models"
+import { useHasCompanyBrain } from "@/hooks/use-company-brain"
+import { useOrgMemberRole } from "@/hooks/use-org-member-role"
+import { dmSans125ClassName } from "@/lib/fonts"
+import { PillButton } from "../integrations/install-steps"
+
+const MODEL_LABELS: Record
= {
+ "claude-sonnet-5": "Sonnet 5",
+ "claude-opus-4.8": "Opus 4.8",
+ "claude-sonnet-4.6": "Sonnet 4.6",
+ "claude-haiku-4.5": "Haiku 4.5",
+ "grok-4.5": "Grok 4.5",
+ "grok-4.3": "Grok 4.3",
+ "grok-4-fast": "Grok 4 Fast",
+ "gpt-5.6": "GPT-5.6",
+ "gpt-5.5": "GPT-5.5",
+}
+
+const labelFor = (id: string) => MODEL_LABELS[id] ?? id
+
+// One-line personality tags so non-experts can tell models apart.
+const MODEL_TAGS: Record = {
+ "claude-sonnet-5": "balanced",
+ "claude-opus-4.8": "most capable, slower",
+ "claude-sonnet-4.6": "balanced",
+ "claude-haiku-4.5": "fast and light",
+ "grok-4.5": "sharp all-rounder",
+ "grok-4.3": "capable",
+ "grok-4-fast": "fastest",
+ "gpt-5.6": "capable",
+ "gpt-5.5": "capable",
+}
+
+const EFFORT_LABELS: Record = {
+ low: "Low",
+ medium: "Medium",
+ high: "High",
+ xhigh: "Extra high",
+}
+
+const ROWS: {
+ role: BrainModelRole
+ effortKey: BrainReasoningKey
+ title: string
+ help: string
+ effortHelp: string
+}[] = [
+ {
+ role: "main",
+ effortKey: "mainEffort",
+ title: "Answers",
+ help: "Writes the replies your brain sends in Slack.",
+ effortHelp: "Deeper thinking gives better answers but takes longer.",
+ },
+ {
+ role: "triage",
+ effortKey: "triageEffort",
+ title: "When to reply",
+ help: "Decides whether and how the brain responds to a message.",
+ effortHelp:
+ "Deeper thinking routes messages more carefully but takes longer.",
+ },
+ {
+ role: "research",
+ effortKey: "researchEffort",
+ title: "Web research",
+ help: "Looks things up on the web when researching your company.",
+ effortHelp: "Deeper research per web search, at the cost of speed.",
+ },
+]
+
+type FullConfig = Required
+
+type PresetDef = {
+ id: string
+ label: string
+ description: string
+ build: (
+ defaults: BrainModelConfig,
+ choices: { main: string[] } & Partial<
+ Record
+ >,
+ ) => FullConfig
+}
+
+const pickEffort = (
+ options: BrainReasoningEffort[] | undefined,
+ wanted: BrainReasoningEffort,
+ fallback: BrainReasoningEffort,
+): BrainReasoningEffort =>
+ !options || options.length === 0 || options.includes(wanted)
+ ? wanted
+ : fallback
+
+const PRESETS: PresetDef[] = [
+ {
+ id: "fast",
+ label: "Fastest",
+ description: "Snappy replies, lighter on credits. Best for quick lookups.",
+ build: (defaults, choices) => ({
+ main: choices.main.includes("grok-4-fast")
+ ? "grok-4-fast"
+ : defaults.main,
+ triage: defaults.triage,
+ research: defaults.research,
+ mainEffort: pickEffort(choices.mainEffort, "low", "low"),
+ triageEffort: pickEffort(choices.triageEffort, "low", "low"),
+ researchEffort: pickEffort(choices.researchEffort, "low", "low"),
+ }),
+ },
+ {
+ id: "balanced",
+ label: "Balanced",
+ description: "Our recommended mix of speed and answer quality.",
+ build: (defaults) => ({
+ main: defaults.main,
+ triage: defaults.triage,
+ research: defaults.research,
+ mainEffort: defaults.mainEffort ?? "high",
+ triageEffort: defaults.triageEffort ?? "low",
+ researchEffort: defaults.researchEffort ?? "high",
+ }),
+ },
+ {
+ id: "thorough",
+ label: "Most thorough",
+ description: "Deepest answers and research. Slower, uses more credits.",
+ build: (defaults, choices) => ({
+ main: defaults.main,
+ triage: defaults.triage,
+ research: defaults.research,
+ mainEffort: pickEffort(choices.mainEffort, "xhigh", "high"),
+ triageEffort: pickEffort(choices.triageEffort, "medium", "low"),
+ researchEffort: pickEffort(choices.researchEffort, "xhigh", "high"),
+ }),
+ },
+]
+
+const CONFIG_KEYS = [
+ "main",
+ "triage",
+ "research",
+ "mainEffort",
+ "triageEffort",
+ "researchEffort",
+] as const
+
+const extraHighIsBounded = (model: string): boolean =>
+ model.startsWith("grok-") || model.startsWith("gpt-")
+
+const controlClass = cn(
+ dmSans125ClassName(),
+ "h-9 w-full rounded-full border border-[#1E293B] bg-[#0D121A] px-3.5 text-[13px] text-[#FAFAFA] outline-none disabled:opacity-50",
+)
+const fieldLabel = cn(
+ dmSans125ClassName(),
+ "text-[11px] font-medium uppercase tracking-[0.06em] text-[#5B6675]",
+)
+const selectContentClass = cn(
+ dmSans125ClassName(),
+ "rounded-[10px] border-white/[0.08] bg-[#1B1F24] text-[#FAFAFA] shadow-[0px_8px_24px_rgba(0,0,0,0.5)]",
+)
+const selectItemClass =
+ "cursor-pointer rounded-[8px] text-[13px] text-[#FAFAFA] hover:bg-white/10 hover:text-white data-[highlighted]:bg-white/10 data-[highlighted]:text-white focus:bg-white/10 focus:text-white"
+
+function SectionTitle({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default function CompanyBrainModels({
+ showHeading = true,
+}: {
+ showHeading?: boolean
+}) {
+ const isCompanyBrain = useHasCompanyBrain()
+ const { isAdmin } = useOrgMemberRole(isCompanyBrain)
+
+ const modelsQuery = useBrainModels(isCompanyBrain)
+ const update = useUpdateBrainModels()
+
+ const [draft, setDraft] = useState>({})
+ const [advancedOpen, setAdvancedOpen] = useState(false)
+
+ const resolved = modelsQuery.data?.resolved
+ const defaults = modelsQuery.data?.defaults
+ const choices = modelsQuery.data?.choices
+
+ const valueFor = (role: BrainModelRole): string =>
+ draft[role] ?? resolved?.[role] ?? ""
+ const effortFor = (key: BrainReasoningKey): BrainReasoningEffort | "" =>
+ draft[key] ?? resolved?.[key] ?? defaults?.[key] ?? ""
+
+ const dirty = useMemo(() => {
+ if (!resolved) return false
+ return ROWS.some(({ role, effortKey }) => {
+ const modelChanged =
+ draft[role] !== undefined && draft[role] !== resolved[role]
+ const effortChanged =
+ draft[effortKey] !== undefined &&
+ draft[effortKey] !== resolved[effortKey]
+ return modelChanged || effortChanged
+ })
+ }, [draft, resolved])
+
+ const presets = useMemo(() => {
+ if (!defaults || !choices) return []
+ return PRESETS.map((p) => ({ ...p, config: p.build(defaults, choices) }))
+ }, [defaults, choices])
+
+ // Preset whose full config matches what's currently on screen (draft over saved).
+ const activePresetId = useMemo(() => {
+ if (!resolved) return null
+ const current: Record = {}
+ for (const key of CONFIG_KEYS) {
+ current[key] = draft[key] ?? resolved[key] ?? defaults?.[key]
+ }
+ return (
+ presets.find((p) =>
+ CONFIG_KEYS.every((key) => current[key] === p.config[key]),
+ )?.id ?? null
+ )
+ }, [presets, draft, resolved, defaults])
+
+ if (!isCompanyBrain) return null
+
+ const disabled = !isAdmin || modelsQuery.isLoading || update.isPending
+
+ return (
+
+ {showHeading ? (
+
+ Models
+
+ Choose which models Company Brain uses. Applies to this organization
+ only.
+
+
+ ) : null}
+
+ {modelsQuery.isLoading ? (
+
+
+ Loading models…
+
+ ) : modelsQuery.isError ? (
+
+ Couldn't load models.
+
+ ) : (
+
+
+ {presets.map((preset) => {
+ const isActive = preset.id === activePresetId
+ return (
+ {
+ if (!resolved) return
+ setDraft({ ...preset.config })
+ }}
+ className={cn(
+ dmSans125ClassName(),
+ "flex min-w-0 cursor-pointer flex-col gap-1.5 rounded-xl p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
+ "bg-[#14161A] shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
+ isActive
+ ? "bg-[#10161f] ring-1 ring-[#2261CA]/45"
+ : "hover:bg-[#171A1F]",
+ )}
+ >
+
+
+ {preset.label}
+ {preset.id === "balanced" ? (
+
+ Recommended
+
+ ) : null}
+
+ {isActive ? (
+
+ ) : null}
+
+
+ {preset.description}
+
+
+ )
+ })}
+
+
+
setAdvancedOpen((open) => !open)}
+ className={cn(
+ dmSans125ClassName(),
+ "flex w-fit cursor-pointer items-center gap-1.5 rounded-full px-1 py-1 text-[12px] font-medium text-[#8B929E] transition-colors hover:text-[#FAFAFA]",
+ )}
+ >
+
+ Advanced
+ {activePresetId === null ? (
+
+ · custom settings in use
+
+ ) : null}
+
+
+ {advancedOpen || activePresetId === null ? (
+
+ {ROWS.map(({ role, effortKey, title, help, effortHelp }) => {
+ const options = choices?.[role] ?? []
+ const current = valueFor(role)
+ const effortOptions = choices?.[effortKey] ?? []
+ const currentEffort = effortFor(effortKey)
+ const isDefault =
+ defaults?.[role] === current &&
+ (effortOptions.length === 0 ||
+ defaults?.[effortKey] === currentEffort)
+ return (
+
+
+
+
+ {title}
+
+
+ {help}
+
+
+ {isDefault ? (
+
+ Default
+
+ ) : null}
+
+
+
+ Model
+
+ setDraft((d) => ({ ...d, [role]: v }))
+ }
+ >
+
+
+
+
+ {options.map((id) => (
+
+ {labelFor(id)}
+ {MODEL_TAGS[id] ? (
+
+ {" "}
+ · {MODEL_TAGS[id]}
+
+ ) : null}
+ {defaults?.[role] === id ? " (default)" : ""}
+
+ ))}
+
+
+
+
+ {effortOptions.length > 0 ? (
+
+
Thinking depth
+
+ {effortOptions.map((effort) => {
+ const isOn = currentEffort === effort
+ return (
+
+ setDraft((currentDraft) => ({
+ ...currentDraft,
+ [effortKey]: effort,
+ }))
+ }
+ className={cn(
+ dmSans125ClassName(),
+ "h-7 min-w-0 flex-1 cursor-pointer rounded-full px-1 text-[11.5px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
+ isOn
+ ? "bg-white/[0.10] text-[#FAFAFA]"
+ : "text-[#8B929E] hover:text-[#FAFAFA]",
+ )}
+ >
+ {EFFORT_LABELS[effort]}
+
+ )
+ })}
+
+
+ Faster
+ Smarter
+
+
+ {effortHelp}
+
+ {currentEffort === "xhigh" &&
+ extraHighIsBounded(current) ? (
+
+ Extra high maps to High for this model provider.
+
+ ) : null}
+
+ ) : null}
+
+ )
+ })}
+
+ ) : null}
+
+ {!isAdmin ? (
+
+
+ Only organization admins can change these.
+
+ ) : (
+
+ {dirty ? (
+
setDraft({})}
+ disabled={update.isPending}
+ className={cn(
+ dmSans125ClassName(),
+ "h-9 rounded-full px-3 text-[13px] font-medium text-[#8B929E] transition-colors hover:text-[#FAFAFA] disabled:opacity-45",
+ )}
+ >
+ Reset
+
+ ) : null}
+
{
+ const patch: Partial = {}
+ for (const { role, effortKey } of ROWS) {
+ if (draft[role] && draft[role] !== resolved?.[role]) {
+ patch[role] = draft[role]
+ }
+ if (
+ draft[effortKey] &&
+ draft[effortKey] !== resolved?.[effortKey]
+ ) {
+ patch[effortKey] = draft[effortKey]
+ }
+ }
+ update.mutate(patch, { onSuccess: () => setDraft({}) })
+ }}
+ >
+ {update.isPending ? (
+
+ ) : null}
+ Save
+
+
+ )}
+
+ )}
+
+ )
+}
diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx
index fa1db533..62d1e468 100644
--- a/apps/web/components/settings/settings-content.tsx
+++ b/apps/web/components/settings/settings-content.tsx
@@ -10,9 +10,6 @@ import Account from "@/components/settings/account"
import Billing from "@/components/settings/billing"
import Integrations from "@/components/settings/integrations"
import ConnectionsMCP from "@/components/settings/connections-mcp"
-import CompanyBrainConnections from "@/components/settings/company-brain-connections"
-import { ProactivenessIcon } from "@/components/settings/proactiveness-icon"
-import Proactiveness from "@/components/settings/proactiveness"
import Support from "@/components/settings/support"
import { ErrorBoundary } from "@/components/error-boundary"
import { useRouter } from "next/navigation"
@@ -54,8 +51,6 @@ export const TABS = [
"billing",
"integrations",
"connections",
- "company-brain",
- "proactiveness",
"support",
] as const
export type SettingsTab = (typeof TABS)[number]
@@ -92,18 +87,6 @@ const NAV_ITEMS: NavItem[] = [
description: "Drive, Notion, OneDrive, MCP",
icon: ,
},
- {
- id: "company-brain",
- label: "Company Brain",
- description: "Connect apps to your brain — org and personal",
- icon: ,
- },
- {
- id: "proactiveness",
- label: "Proactiveness",
- description: "Scheduled digests and unprompted actions",
- icon: ,
- },
{
id: "support",
label: "Support & Help",
@@ -166,7 +149,7 @@ export function SettingsContent({
const { user, org, organizations, setActiveOrg, clearActiveOrg } = useAuth()
const isCompanyBrain = useHasCompanyBrain()
- // Company Brain orgs manage tools inside Company Brain; hide the generic tabs.
+ // Company Brain orgs manage tools in the Configure view; hide the generic tabs.
const navItems = isCompanyBrain
? NAV_ITEMS.filter(
(item) => item.id !== "integrations" && item.id !== "connections",
@@ -498,8 +481,6 @@ export function SettingsContent({
{activeTab === "billing" && }
{activeTab === "integrations" && }
{activeTab === "connections" && }
- {activeTab === "company-brain" && }
- {activeTab === "proactiveness" && }
{activeTab === "support" && }
diff --git a/apps/web/components/user-profile-menu.tsx b/apps/web/components/user-profile-menu.tsx
index d4ba056f..b69701f2 100644
--- a/apps/web/components/user-profile-menu.tsx
+++ b/apps/web/components/user-profile-menu.tsx
@@ -15,10 +15,10 @@ import { useRouter } from "next/navigation"
import {
LogOut,
Settings,
+ Settings2,
RotateCcw,
HelpCircle,
LifeBuoy,
- Building2,
Sun,
} from "lucide-react"
import { cn } from "@lib/utils"
@@ -26,7 +26,6 @@ import { dmSansClassName } from "@/lib/fonts"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { useTokenUsage } from "@/hooks/use-token-usage"
import { useSettingsModal } from "@/components/settings/settings-modal"
-import { ProactivenessIcon } from "@/components/settings/proactiveness-icon"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
import { useViewMode } from "@/lib/view-mode-context"
@@ -165,20 +164,13 @@ export function UserProfileMenu({
Settings
- openSettings("company-brain")}
- className="gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-white/85 hover:bg-white/[0.06] focus:bg-white/[0.06] focus:text-white cursor-pointer"
- >
-
- Company Brain
-
{isCompanyBrain ? (
openSettings("proactiveness")}
+ onClick={() => void setViewMode("configure")}
className="gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-white/85 hover:bg-white/[0.06] focus:bg-white/[0.06] focus:text-white cursor-pointer"
>
-
- Proactiveness
+
+ Configure
) : null}
{isCompanyBrain ? (
diff --git a/apps/web/hooks/use-brain-models.ts b/apps/web/hooks/use-brain-models.ts
new file mode 100644
index 00000000..95651d52
--- /dev/null
+++ b/apps/web/hooks/use-brain-models.ts
@@ -0,0 +1,68 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
+import { toast } from "sonner"
+import { useAuth } from "@lib/auth-context"
+
+const BACKEND =
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
+const BASE = `${BACKEND}/brain/models`
+
+export type BrainModelRole = "main" | "triage" | "research"
+export type BrainReasoningEffort = "low" | "medium" | "high" | "xhigh"
+export type BrainReasoningKey = "mainEffort" | "triageEffort" | "researchEffort"
+
+export type BrainModelConfig = Record &
+ Partial>
+
+export type BrainModelsResponse = {
+ resolved: BrainModelConfig
+ defaults: BrainModelConfig
+ choices: Record &
+ Partial>
+}
+
+export function useBrainModels(enabled: boolean) {
+ const { org } = useAuth()
+ return useQuery({
+ queryKey: ["brain", "models", org?.id],
+ queryFn: async (): Promise => {
+ const res = await fetch(`${BASE}/`, { credentials: "include" })
+ if (!res.ok) throw new Error("Failed to load models")
+ return res.json()
+ },
+ enabled,
+ staleTime: 60_000,
+ })
+}
+
+export function useUpdateBrainModels() {
+ const { org } = useAuth()
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: async (patch: Partial) => {
+ const res = await fetch(`${BASE}/`, {
+ method: "PATCH",
+ credentials: "include",
+ headers: { "Content-Type": "application/json", "X-App-Source": "nova" },
+ body: JSON.stringify(patch),
+ })
+ if (res.status === 403)
+ throw new Error("Only admins can change brain models.")
+ if (!res.ok) {
+ const b = (await res.json().catch(() => ({}))) as {
+ message?: string
+ error?: string
+ }
+ throw new Error(b.message ?? b.error ?? "Failed to save models")
+ }
+ return res.json()
+ },
+ onSuccess: () => {
+ void queryClient.invalidateQueries({
+ queryKey: ["brain", "models", org?.id],
+ })
+ toast.success("Brain models saved")
+ },
+ onError: (err) =>
+ toast.error(err instanceof Error ? err.message : "Failed to save models"),
+ })
+}
diff --git a/apps/web/hooks/use-document-mutations.ts b/apps/web/hooks/use-document-mutations.ts
index c0b1c864..7f0e6384 100644
--- a/apps/web/hooks/use-document-mutations.ts
+++ b/apps/web/hooks/use-document-mutations.ts
@@ -390,6 +390,7 @@ export function useDocumentMutations({
urls: string[]
project: string
}): Promise<{ success: number; failed: number }> => {
+ const entityContext = await resolveEntityContext(project)
let success = 0
let failed = 0
@@ -400,7 +401,7 @@ export function useDocumentMutations({
documents: chunk.map((url) => ({
content: url,
containerTags: [project],
- entityContext,
+ ...(entityContext !== undefined ? { entityContext } : {}),
metadata: { sm_source: "consumer" },
})),
},
diff --git a/apps/web/hooks/use-org-member-role.ts b/apps/web/hooks/use-org-member-role.ts
new file mode 100644
index 00000000..66475e0c
--- /dev/null
+++ b/apps/web/hooks/use-org-member-role.ts
@@ -0,0 +1,19 @@
+import { useQuery } from "@tanstack/react-query"
+import { authClient } from "@lib/auth"
+import { useAuth } from "@lib/auth-context"
+
+// Shared active-member role for the current org. Single queryKey so the
+// company-brain settings sections dedupe the getActiveMember call.
+export function useOrgMemberRole(enabled = true) {
+ const { org } = useAuth()
+ const query = useQuery({
+ queryKey: ["org", "member-role", org?.id],
+ queryFn: async () =>
+ (await authClient.organization.getActiveMember()).data?.role ?? null,
+ staleTime: 60_000,
+ enabled: enabled && !!org?.id,
+ })
+ const role = (query.data ?? "").toLowerCase()
+ const isAdmin = role === "owner" || role === "admin"
+ return { role, isAdmin, query }
+}
diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts
index 7ce5b9df..eebd1697 100644
--- a/apps/web/lib/analytics.ts
+++ b/apps/web/lib/analytics.ts
@@ -222,14 +222,7 @@ export const analytics = {
// settings / spaces / docs analytics
settingsTabChanged: (props: {
- tab:
- | "account"
- | "billing"
- | "integrations"
- | "connections"
- | "company-brain"
- | "proactiveness"
- | "support"
+ tab: "account" | "billing" | "integrations" | "connections" | "support"
}) => safeCapture("settings_tab_changed", props),
spaceCreated: () => safeCapture("space_created"),
diff --git a/apps/web/lib/search-params.ts b/apps/web/lib/search-params.ts
index 9afb6eab..2d9f1688 100644
--- a/apps/web/lib/search-params.ts
+++ b/apps/web/lib/search-params.ts
@@ -28,6 +28,7 @@ const viewLiterals = [
"graph",
"list",
"integrations",
+ "configure",
"chat",
"digests",
// Integration sub-views — each card is its own view
diff --git a/bun.lock b/bun.lock
index eb3b53dd..0512d4a0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -332,7 +332,7 @@
},
"packages/tools": {
"name": "@supermemory/tools",
- "version": "2.1.0",
+ "version": "2.1.1",
"dependencies": {
"@ai-sdk/anthropic": "^2.0.25",
"@ai-sdk/openai": "^2.0.23",
diff --git a/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py b/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py
index 9069630e..5359426e 100644
--- a/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py
+++ b/packages/agent-framework-python/src/supermemory_agent_framework/context_provider.py
@@ -9,7 +9,13 @@ following the same pattern as the built-in Mem0 integration.
from typing import Any, Literal, Optional
-from agent_framework import BaseContextProvider
+try:
+ from agent_framework import BaseContextProvider
+except ImportError:
+ # Renamed in agent-framework-core 1.0.0 stable; the interface is
+ # unchanged (source_id __init__, before_run/after_run hooks with
+ # identical keyword-only signatures).
+ from agent_framework import ContextProvider as BaseContextProvider
from .connection import AgentSupermemory
from .utils import (
diff --git a/packages/tools/package.json b/packages/tools/package.json
index 053089e5..59289f3d 100644
--- a/packages/tools/package.json
+++ b/packages/tools/package.json
@@ -1,7 +1,7 @@
{
"name": "@supermemory/tools",
"type": "module",
- "version": "2.1.0",
+ "version": "2.1.1",
"description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory",
"scripts": {
"build": "tsdown",
diff --git a/packages/tools/src/claude-memory.test.ts b/packages/tools/src/claude-memory.test.ts
index 80aea36d..c4f62f16 100644
--- a/packages/tools/src/claude-memory.test.ts
+++ b/packages/tools/src/claude-memory.test.ts
@@ -4,12 +4,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"
// can be exercised deterministically without any network access. We only need
// `search.execute` to return a single document with known multi-line content.
const searchExecute = vi.fn()
+const addMock = vi.fn()
vi.mock("supermemory", () => {
return {
default: class MockSupermemory {
search = { execute: searchExecute }
- add = vi.fn()
+ add = addMock
memories = { forget: vi.fn() }
},
}
@@ -83,3 +84,100 @@ describe("ClaudeMemoryTool view_range", () => {
expect(result.content).not.toContain("line5")
})
})
+
+describe("ClaudeMemoryTool exact-file matching", () => {
+ let tool: ClaudeMemoryTool
+
+ beforeEach(() => {
+ searchExecute.mockReset()
+ addMock.mockReset()
+ tool = new ClaudeMemoryTool("test-api-key")
+ })
+
+ it("view finds the exact file even when a neighbour ranks first", async () => {
+ searchExecute.mockResolvedValue({
+ results: [
+ { documentId: "memories_notes_backup_txt", content: "backup stuff" },
+ { documentId: "memories_notes_txt", content: FILE_CONTENT },
+ ],
+ })
+
+ const result = await tool.handleCommand({
+ command: "view",
+ path: FILE_PATH,
+ })
+
+ expect(result.success).toBe(true)
+ expect(result.content).toContain("line1")
+ expect(result.content).not.toContain("backup stuff")
+ })
+
+ it("view reports not-found instead of returning a different file", async () => {
+ // Semantic search can surface a similarly-named file; that must not
+ // be served as the requested one.
+ searchExecute.mockResolvedValue({
+ results: [
+ { documentId: "memories_notes_backup_txt", content: "backup stuff" },
+ ],
+ })
+
+ const result = await tool.handleCommand({
+ command: "view",
+ path: FILE_PATH,
+ })
+
+ expect(result.success).toBe(false)
+ expect(result.error).toContain("File not found")
+ })
+
+ it("str_replace refuses to modify a different file than requested", async () => {
+ searchExecute.mockResolvedValue({
+ results: [
+ { documentId: "memories_notes_backup_txt", content: "backup stuff" },
+ ],
+ })
+
+ const result = await tool.handleCommand({
+ command: "str_replace",
+ path: FILE_PATH,
+ old_str: "backup",
+ new_str: "primary",
+ })
+
+ expect(result.success).toBe(false)
+ expect(addMock).not.toHaveBeenCalled()
+ })
+})
+
+describe("ClaudeMemoryTool str_replace replacement literalness", () => {
+ let tool: ClaudeMemoryTool
+
+ beforeEach(() => {
+ searchExecute.mockReset()
+ addMock.mockReset()
+ searchExecute.mockResolvedValue({
+ results: [{ documentId: "memories_notes_txt", content: FILE_CONTENT }],
+ })
+ tool = new ClaudeMemoryTool("test-api-key")
+ })
+
+ it.each([
+ "$&",
+ "$'",
+ "$`",
+ "$$",
+ ])("stores %s literally instead of expanding it as a replacement pattern", async (dollarSequence) => {
+ const result = await tool.handleCommand({
+ command: "str_replace",
+ path: FILE_PATH,
+ old_str: "line3",
+ new_str: `price is ${dollarSequence} today`,
+ })
+
+ expect(result.success).toBe(true)
+ expect(addMock).toHaveBeenCalledTimes(1)
+ const stored = addMock.mock.calls[0]?.[0]?.content as string
+ expect(stored).toContain(`price is ${dollarSequence} today`)
+ expect(stored).not.toContain("line3")
+ })
+})
diff --git a/packages/tools/src/claude-memory.ts b/packages/tools/src/claude-memory.ts
index 6830b296..8c665701 100644
--- a/packages/tools/src/claude-memory.ts
+++ b/packages/tools/src/claude-memory.ts
@@ -262,29 +262,21 @@ export class ClaudeMemoryTool {
viewRange?: [number, number],
): Promise {
try {
- const normalizedId = this.normalizePathToCustomId(filePath)
-
- const response = await this.client.search.execute({
- q: normalizedId,
- containerTags: this.containerTags,
- limit: 1,
- includeFullDocs: true,
- })
-
- // Try to find exact match by customId
- const exactMatch = response.results?.find(
- (r) => r.documentId === normalizedId,
- )
- const document = exactMatch || response.results?.[0]
-
- if (!document) {
+ // Same lookup as every mutating command: limit 5 so the exact
+ // customId match is findable among semantic near-neighbours.
+ // With the old limit of 1, a similarly-named file ranking first
+ // made this return the wrong file's contents as a success.
+ const readResult = await this.getFileDocument(filePath)
+ if (!readResult.success || !readResult.document) {
return {
success: false,
- error: `File not found: ${filePath}`,
+ error: readResult.error || `File not found: ${filePath}`,
}
}
- let content = document.content || ""
+ const document = readResult.document
+
+ let content: string = document.raw || document.content || ""
// Apply line range if specified
if (viewRange) {
@@ -393,8 +385,10 @@ export class ClaudeMemoryTool {
}
}
- // Replace the string
- const newContent = originalContent.replace(oldStr, newStr)
+ // Replace the string. The function replacer keeps `$` sequences
+ // in the replacement literal — a bare string here would expand
+ // patterns like $&, $', and $` and silently corrupt the file.
+ const newContent = originalContent.replace(oldStr, () => newStr)
// Update the document
const normalizedId = this.normalizePathToCustomId(filePath)
@@ -590,11 +584,12 @@ export class ClaudeMemoryTool {
includeFullDocs: true,
})
- // Try to find exact match by customId first
- const exactMatch = response.results?.find(
+ // Only accept the exact customId match. Falling back to the top
+ // semantic hit would let callers read — and worse, modify or
+ // delete — a different file than the one they asked for.
+ const document = response.results?.find(
(r) => r.documentId === normalizedId,
)
- const document = exactMatch || response.results?.[0]
if (!document) {
return {
diff --git a/packages/tools/src/openai/middleware.ts b/packages/tools/src/openai/middleware.ts
index 7fc4267e..33ac373a 100644
--- a/packages/tools/src/openai/middleware.ts
+++ b/packages/tools/src/openai/middleware.ts
@@ -399,7 +399,7 @@ const addMemoryTool = async (
* @param options.customId - Optional conversation ID to group messages for contextual memory generation
* @param options.verbose - Enable detailed logging of memory operations (default: false)
* @param options.mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both) (default: "profile")
- * @param options.addMemory - Automatic memory storage mode: "always" or "never" (default: "never")
+ * @param options.addMemory - Automatic memory storage mode: "always" or "never" (default: "always")
* @returns Object with `wrapClient` and `createClient` methods
* @throws {Error} When SUPERMEMORY_API_KEY environment variable is not set
*