Nicholai 7f5efb84e2 feat(agent): migrate to Anthropic Agents SDK
Replace Vercel AI SDK with Anthropic Claude Agent SDK.
Add standalone agent server (packages/agent-server/)
with MCP tools, JWT auth, and SSE streaming. Introduce
bridge API routes (src/app/api/compass/) and custom
SSE hooks (use-agent, use-compass-chat) replacing
useChat. Remove provider.ts, tools.ts, system-prompt.ts,
github-tools.ts, usage.ts, and old agent route.
2026-02-16 18:37:26 -07:00

338 lines
9.7 KiB
TypeScript

import { getCloudflareContext } from "@opennextjs/cloudflare"
import { validateAgentAuth } from "@/lib/agent/api-auth"
import {
getCustomThemes,
setUserThemePreference,
saveCustomTheme,
getCustomThemeById,
} from "@/app/actions/themes"
import { THEME_PRESETS, findPreset } from "@/lib/theme/presets"
import type {
ThemeDefinition,
ColorMap,
ThemeFonts,
ThemeTokens,
ThemeShadows,
} from "@/lib/theme/types"
type ThemeAction = "list" | "set" | "generate" | "edit"
export async function POST(req: Request): Promise<Response> {
const { env } = await getCloudflareContext()
const envRecord = env as unknown as Record<string, string>
const auth = await validateAgentAuth(req, envRecord)
if (!auth.valid) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
})
}
let body: { action: ThemeAction; [key: string]: unknown }
try {
body = await req.json()
} catch {
return new Response(JSON.stringify({ error: "Invalid JSON body" }), {
status: 400,
headers: { "Content-Type": "application/json" },
})
}
try {
switch (body.action) {
case "list": {
const presets = THEME_PRESETS.map((p) => ({
id: p.id,
name: p.name,
description: p.description,
isPreset: true,
}))
const customResult = await getCustomThemes()
const customs = customResult.success
? customResult.data.map((c) => ({
id: c.id,
name: c.name,
description: c.description,
isPreset: false,
}))
: []
return new Response(
JSON.stringify({ themes: [...presets, ...customs] }),
{
headers: { "Content-Type": "application/json" },
}
)
}
case "set": {
const themeId = body.themeId as string
if (!themeId) {
return new Response(
JSON.stringify({ error: "themeId required" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
)
}
const result = await setUserThemePreference(themeId)
if (!result.success) {
return new Response(
JSON.stringify({ error: result.error }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
)
}
return new Response(
JSON.stringify({
success: true,
themeId,
}),
{
headers: { "Content-Type": "application/json" },
}
)
}
case "generate": {
const name = body.name as string
const description = body.description as string
const light = body.light as Record<string, string>
const dark = body.dark as Record<string, string>
const fonts = body.fonts as { sans: string; serif: string; mono: string }
const googleFonts = (body.googleFonts as string[]) ?? []
const radius = (body.radius as string) ?? "0.5rem"
const spacing = (body.spacing as string) ?? "0.25rem"
if (!name || !description || !light || !dark || !fonts) {
return new Response(
JSON.stringify({ error: "Missing required fields" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
)
}
const nativePreset = findPreset("native-compass")
if (!nativePreset) {
return new Response(
JSON.stringify({ error: "Internal error" }),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
)
}
const tokens: ThemeTokens = {
radius,
spacing,
trackingNormal: "0em",
shadowColor: "#000000",
shadowOpacity: "0.1",
shadowBlur: "3px",
shadowSpread: "0px",
shadowOffsetX: "0",
shadowOffsetY: "1px",
}
const defaultShadows: ThemeShadows = {
"2xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)",
xs: "0 1px 3px 0px hsl(0 0% 0% / 0.05)",
sm: "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)",
default:
"0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)",
md: "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10)",
lg: "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10)",
xl: "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10)",
"2xl": "0 1px 3px 0px hsl(0 0% 0% / 0.25)",
}
const theme: ThemeDefinition = {
id: "",
name,
description,
light: light as unknown as ColorMap,
dark: dark as unknown as ColorMap,
fonts: fonts as ThemeFonts,
fontSources: {
googleFonts,
},
tokens,
shadows: { light: defaultShadows, dark: defaultShadows },
isPreset: false,
previewColors: {
primary: light["primary"] ?? "oklch(0.5 0.1 200)",
background: light["background"] ?? "oklch(0.97 0 0)",
foreground: light["foreground"] ?? "oklch(0.2 0 0)",
},
}
const saveResult = await saveCustomTheme(
name,
description,
JSON.stringify(theme),
)
if (!saveResult.success) {
return new Response(
JSON.stringify({ error: saveResult.error }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
)
}
const savedTheme = { ...theme, id: saveResult.id }
return new Response(
JSON.stringify({
success: true,
themeId: saveResult.id,
themeData: savedTheme,
}),
{
headers: { "Content-Type": "application/json" },
}
)
}
case "edit": {
const themeId = body.themeId as string
if (!themeId) {
return new Response(
JSON.stringify({ error: "themeId required" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
)
}
const existing = await getCustomThemeById(themeId)
if (!existing.success) {
return new Response(
JSON.stringify({ error: existing.error }),
{
status: 404,
headers: { "Content-Type": "application/json" },
}
)
}
const prev = JSON.parse(
existing.data.themeData,
) as ThemeDefinition
const mergedLight = body.light
? ({
...prev.light,
...(body.light as Record<string, string>),
} as unknown as ColorMap)
: prev.light
const mergedDark = body.dark
? ({
...prev.dark,
...(body.dark as Record<string, string>),
} as unknown as ColorMap)
: prev.dark
const mergedFonts: ThemeFonts = body.fonts
? {
sans:
(body.fonts as { sans?: string }).sans ?? prev.fonts.sans,
serif:
(body.fonts as { serif?: string }).serif ??
prev.fonts.serif,
mono:
(body.fonts as { mono?: string }).mono ?? prev.fonts.mono,
}
: prev.fonts
const mergedTokens: ThemeTokens = {
...prev.tokens,
...(body.radius ? { radius: body.radius as string } : {}),
...(body.spacing ? { spacing: body.spacing as string } : {}),
}
const mergedFontSources = body.googleFonts
? { googleFonts: body.googleFonts as string[] }
: prev.fontSources
const name = (body.name as string) ?? existing.data.name
const description =
(body.description as string) ?? existing.data.description
const merged: ThemeDefinition = {
...prev,
id: themeId,
name,
description,
light: mergedLight,
dark: mergedDark,
fonts: mergedFonts,
fontSources: mergedFontSources,
tokens: mergedTokens,
previewColors: {
primary: mergedLight.primary,
background: mergedLight.background,
foreground: mergedLight.foreground,
},
}
const saveResult = await saveCustomTheme(
name,
description,
JSON.stringify(merged),
themeId,
)
if (!saveResult.success) {
return new Response(
JSON.stringify({ error: saveResult.error }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
)
}
return new Response(
JSON.stringify({
success: true,
themeId,
themeData: merged,
}),
{
headers: { "Content-Type": "application/json" },
}
)
}
default:
return new Response(
JSON.stringify({ error: "Unknown action" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
)
}
} catch (error) {
console.error("Themes endpoint error:", error)
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "Internal error",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
)
}
}