From 5b248e32ac2f88682ff8011ceccba5993848cd7e Mon Sep 17 00:00:00 2001 From: Yash Dewasthale Date: Mon, 27 Jul 2026 17:06:00 +0530 Subject: [PATCH] feat: implement API for AI interactions and authentication: - Added core API functionality for AI chat and object generation with streaming responses. - Introduced authentication middleware to secure routes and manage user sessions. - Created health check and user management endpoints for improved service monitoring. - Established a structured environment configuration for better deployment practices. - Integrated token budget management to track usage and enforce limits on AI interactions. --- apps/api/.env.example | 30 +++++ apps/api/package.json | 24 +++- apps/api/src/app.ts | 29 +++++ apps/api/src/index.ts | 14 +++ apps/api/src/lib/auth.ts | 21 ++++ apps/api/src/lib/pricing.ts | 37 ++++++ apps/api/src/lib/prisma.ts | 2 + apps/api/src/lib/token-budget.ts | 47 ++++++++ apps/api/src/lib/track-usage.ts | 27 +++++ apps/api/src/middleware/auth.ts | 28 +++++ apps/api/src/providers/index.ts | 193 +++++++++++++++++++++++++++++++ apps/api/src/routes/ai.ts | 135 +++++++++++++++++++++ apps/api/src/routes/auth.ts | 20 ++++ apps/api/src/routes/health.ts | 9 ++ apps/api/src/types.ts | 24 ++++ apps/api/tsconfig.json | 25 ++++ bun.lock | 80 ++++++++++++- packages/sdk/client.ts | 161 ++++++++++++++++++++++++++ packages/sdk/index.ts | 30 +++++ packages/sdk/package.json | 8 +- packages/sdk/schemas.ts | 79 +++++++++++++ packages/sdk/tsconfig.json | 16 +++ packages/sdk/types.ts | 90 ++++++++++++++ 23 files changed, 1120 insertions(+), 9 deletions(-) create mode 100644 apps/api/.env.example create mode 100644 apps/api/src/app.ts create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/lib/auth.ts create mode 100644 apps/api/src/lib/pricing.ts create mode 100644 apps/api/src/lib/prisma.ts create mode 100644 apps/api/src/lib/token-budget.ts create mode 100644 apps/api/src/lib/track-usage.ts create mode 100644 apps/api/src/middleware/auth.ts create mode 100644 apps/api/src/providers/index.ts create mode 100644 apps/api/src/routes/ai.ts create mode 100644 apps/api/src/routes/auth.ts create mode 100644 apps/api/src/routes/health.ts create mode 100644 apps/api/src/types.ts create mode 100644 apps/api/tsconfig.json create mode 100644 packages/sdk/client.ts create mode 100644 packages/sdk/schemas.ts create mode 100644 packages/sdk/tsconfig.json create mode 100644 packages/sdk/types.ts diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..5bda5b3 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,30 @@ +# Server +PORT=4000 +SUPERCODE_JSON_BODY_LIMIT=10mb + +# Auth (Better-Auth) +BETTER_AUTH_URL=http://localhost:4000 +CLIENT_URL=http://localhost:3000 + +# Database (terminal DB) +DATABASE_URL=postgresql://user:password@localhost:5432/supercode_terminal + +# GitHub OAuth +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# Provider API Keys +GOOGLE_GENERATIVE_AI_API_KEY= +OPENROUTER_API_KEY= +OPENROUTER_MODEL=openai/gpt-oss-120b:free +MINIMAX_API_KEY= +MINIMAX_MAX_TOKENS=4096 +NVIDIA_API_KEY= +NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1 +NVIDIA_MODEL=minimaxai/minimax-m3 +NVIDIA_MAX_TOKENS=8192 +NVIDIA_TEMPERATURE=1 +NVIDIA_TOP_P=0.95 +MERGE_DEV_API_KEY= +ORCAROUTER_API_KEY= +CONCENTRATEAI_API_KEY= diff --git a/apps/api/package.json b/apps/api/package.json index 514b141..c353c21 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -2,10 +2,28 @@ "name": "api", "version": "0.1.0", "private": true, + "type": "module", "scripts": { - "dev": "echo 'api app scaffold pending'", - "build": "echo 'api build scaffold pending'", + "dev": "bun --watch src/index.ts", + "build": "bun build --target bun --outdir dist src/index.ts", + "start": "bun src/index.ts", "lint": "echo 'api lint scaffold pending'", - "typecheck": "echo 'api typecheck scaffold pending'" + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@ai-sdk/google": "^3.0.80", + "@ai-sdk/openai-compatible": "^2.0.48", + "@hono/zod-validator": "^0.5.0", + "@openrouter/ai-sdk-provider": "^2.9.0", + "@super/db-terminal": "workspace:*", + "ai": "^7.0.37", + "better-auth": "^1.6.23", + "hono": "^4.7.0", + "vercel-minimax-ai-provider": "^0.0.2", + "zod": "^3.25.2" + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "typescript": "^5.7.0" } } diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts new file mode 100644 index 0000000..28225d6 --- /dev/null +++ b/apps/api/src/app.ts @@ -0,0 +1,29 @@ +import { Hono } from "hono" +import { cors } from "hono/cors" +import { logger } from "hono/logger" +import { bodyLimit } from "hono/body-limit" +import { healthRoute } from "./routes/health" +import { authRoute } from "./routes/auth" +import { aiRoute } from "./routes/ai" +import type { AppEnv } from "./types" + +export function createApp() { + const app = new Hono().basePath("/api") + + app.use("*", logger()) + app.use("*", cors()) + app.use("*", bodyLimit({ + maxSize: 10 * 1024 * 1024, + })) + + app.onError((err, c) => { + console.error("[API Error]", err) + return c.json({ error: "Internal server error" }, 500) + }) + + app.route("/", healthRoute) + app.route("/", authRoute) + app.route("/", aiRoute) + + return app +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..b910ef8 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,14 @@ +import { serve } from "bun" +import { createApp } from "./app" + +const PORT = Number(process.env.PORT) || 4000 + +serve({ + fetch: createApp().fetch, + port: PORT, + development: { + hmr: true, + }, +}) + +console.log(`[API] Server running on http://localhost:${PORT}`) diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts new file mode 100644 index 0000000..a7cdf94 --- /dev/null +++ b/apps/api/src/lib/auth.ts @@ -0,0 +1,21 @@ +import { betterAuth } from "better-auth" +import { prismaAdapter } from "better-auth/adapters/prisma" +import prisma from "./prisma" + +const serverUrl = process.env.BETTER_AUTH_URL || "http://localhost:4000" +const clientUrl = process.env.CLIENT_URL || "http://localhost:3000" +const isProduction = serverUrl.startsWith("https://") + +export const auth = betterAuth({ + database: prismaAdapter(prisma, { provider: "postgresql" }), + baseURL: serverUrl, + basePath: "/api/auth", + trustedOrigins: [clientUrl, serverUrl], + socialProviders: { + github: { + clientId: process.env.GITHUB_CLIENT_ID as string, + clientSecret: process.env.GITHUB_CLIENT_SECRET as string, + ...(isProduction ? { redirectURI: `${clientUrl}/api/auth/callback/github` } : {}), + }, + }, +}) diff --git a/apps/api/src/lib/pricing.ts b/apps/api/src/lib/pricing.ts new file mode 100644 index 0000000..3f7e26c --- /dev/null +++ b/apps/api/src/lib/pricing.ts @@ -0,0 +1,37 @@ +export interface ModelPricing { + input: number + output: number + cacheRead: number + cacheWrite: number +} + +const MODEL_PRICING: Record = { + "gemini-3-flash-preview": { input: 1, output: 4, cacheRead: 0.25, cacheWrite: 1 }, + "gemini-3-pro-preview": { input: 1.25, output: 10, cacheRead: 0.315, cacheWrite: 1.25 }, + "minimax-m3": { input: 0.15, output: 0.85, cacheRead: 0.015, cacheWrite: 0.15 }, + "nvidia/minimax-m3": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, +} + +export function lookupPricing(model: string): ModelPricing { + return ( + MODEL_PRICING[model] ?? { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + } + ) +} + +export function computeCost( + model: string, + inputTokens: number, + outputTokens: number, + cachedInputTokens: number +): number { + const p = lookupPricing(model) + const regularInput = inputTokens - cachedInputTokens + const cost = + (regularInput * p.input + cachedInputTokens * p.cacheRead + outputTokens * p.output) / 1_000_000 + return Math.round(cost * 1_000_000) / 1_000_000 +} diff --git a/apps/api/src/lib/prisma.ts b/apps/api/src/lib/prisma.ts new file mode 100644 index 0000000..93ec10d --- /dev/null +++ b/apps/api/src/lib/prisma.ts @@ -0,0 +1,2 @@ +import prisma from "@super/db-terminal" +export default prisma diff --git a/apps/api/src/lib/token-budget.ts b/apps/api/src/lib/token-budget.ts new file mode 100644 index 0000000..1fcac02 --- /dev/null +++ b/apps/api/src/lib/token-budget.ts @@ -0,0 +1,47 @@ +import prisma from "./prisma" + +const DAILY_TOKEN_LIMIT = 128_000 +const DAILY_QUERY_LIMIT = 20 + +export async function checkDailyTokenBudget(userId: string): Promise<{ + allowed: boolean + used: number + limit: number + remaining: number + queriesUsed: number + queriesRemaining: number +}> { + const today = new Date() + today.setHours(0, 0, 0, 0) + const tomorrow = new Date(today) + tomorrow.setDate(tomorrow.getDate() + 1) + + const events = await prisma.usageEvent.findMany({ + where: { + createdAt: { gte: today, lt: tomorrow }, + }, + select: { + inputTokens: true, + outputTokens: true, + cachedInputTokens: true, + }, + }) + + const totalTokens = events.reduce( + (sum, e) => sum + e.inputTokens + e.outputTokens + (e.cachedInputTokens ?? 0), + 0 + ) + + const queriesUsed = events.length + const remaining = DAILY_TOKEN_LIMIT - totalTokens + const queriesRemaining = DAILY_QUERY_LIMIT - queriesUsed + + return { + allowed: remaining > 0 && queriesRemaining > 0, + used: totalTokens, + limit: DAILY_TOKEN_LIMIT, + remaining: Math.max(0, remaining), + queriesUsed, + queriesRemaining: Math.max(0, queriesRemaining), + } +} diff --git a/apps/api/src/lib/track-usage.ts b/apps/api/src/lib/track-usage.ts new file mode 100644 index 0000000..527fb6f --- /dev/null +++ b/apps/api/src/lib/track-usage.ts @@ -0,0 +1,27 @@ +import prisma from "./prisma" + +export async function recordUsage(opts: { + provider: string + model: string + inputTokens: number + outputTokens: number + cachedInputTokens?: number + costUsd: number + durationMs: number +}) { + try { + await prisma.usageEvent.create({ + data: { + provider: opts.provider, + model: opts.model, + inputTokens: opts.inputTokens, + outputTokens: opts.outputTokens, + cachedInputTokens: opts.cachedInputTokens ?? 0, + costUsd: opts.costUsd, + durationMs: opts.durationMs, + }, + }) + } catch (e) { + console.error("[track-usage] Failed to record usage event:", e) + } +} diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..8848309 --- /dev/null +++ b/apps/api/src/middleware/auth.ts @@ -0,0 +1,28 @@ +import { createMiddleware } from "hono/factory" +import prisma from "../lib/prisma" +import type { AppEnv } from "../types" + +export const authenticate = createMiddleware(async (c, next) => { + const authHeader = c.req.header("Authorization") + if (!authHeader?.startsWith("Bearer ")) { + return c.json({ error: "Unauthorized" }, 401) + } + + const token = authHeader.slice(7) + try { + const session = await prisma.session.findUnique({ + where: { token }, + include: { user: true }, + }) + + if (!session || session.expiresAt < new Date()) { + return c.json({ error: "Unauthorized" }, 401) + } + + c.set("user", session.user) + c.set("session", session) + await next() + } catch { + return c.json({ error: "Unauthorized" }, 401) + } +}) diff --git a/apps/api/src/providers/index.ts b/apps/api/src/providers/index.ts new file mode 100644 index 0000000..80a28e0 --- /dev/null +++ b/apps/api/src/providers/index.ts @@ -0,0 +1,193 @@ +import { streamText, type ModelMessage, type Tool } from "ai" +import { google } from "@ai-sdk/google" +import { createOpenRouter } from "@openrouter/ai-sdk-provider" +import { createOpenAICompatible } from "@ai-sdk/openai-compatible" +import { minimax } from "vercel-minimax-ai-provider" +import prisma from "../lib/prisma" + +type StreamResult = ReturnType + +const DAILY_TOKEN_LIMIT = 128_000 +const DAILY_QUERY_LIMIT = 20 + +function buildSystemPrompt(profile?: string | null, projectDoc?: string | null): string { + const parts: string[] = [] + parts.push( + "You are Supercode, a CLI-based AI coding agent that lives in the user's terminal. You are an expert software engineer who writes clean, production-grade code." + ) + if (projectDoc) { + parts.push(`\n## Project Context\n${projectDoc}`) + } + if (profile) { + parts.push(`\n## User Instructions\n${profile}`) + } + parts.push("\n## Rules\n- Be concise and direct\n- Write code, don't explain unless asked\n- Never add unsolicited comments\n- Follow existing codebase conventions") + return parts.join("\n") +} + +async function checkDailyTokenBudget(): Promise<{ + allowed: boolean + remaining: number + queriesRemaining: number +}> { + const today = new Date() + today.setHours(0, 0, 0, 0) + const tomorrow = new Date(today) + tomorrow.setDate(tomorrow.getDate() + 1) + + const events = await prisma.usageEvent.findMany({ + where: { + createdAt: { gte: today, lt: tomorrow }, + }, + select: { + inputTokens: true, + outputTokens: true, + cachedInputTokens: true, + }, + }) + + const totalTokens = events.reduce( + (sum, e) => sum + e.inputTokens + e.outputTokens + (e.cachedInputTokens ?? 0), + 0 + ) + + return { + allowed: totalTokens < DAILY_TOKEN_LIMIT && events.length < DAILY_QUERY_LIMIT, + remaining: Math.max(0, DAILY_TOKEN_LIMIT - totalTokens), + queriesRemaining: Math.max(0, DAILY_QUERY_LIMIT - events.length), + } +} + +function computeCost( + model: string, + inputTokens: number, + outputTokens: number, + cachedInputTokens: number +): number { + const MODEL_PRICING: Record = { + "gemini-3-flash-preview": { input: 1, output: 4, cacheRead: 0.25 }, + "gemini-3-pro-preview": { input: 1.25, output: 10, cacheRead: 0.315 }, + "minimax-m3": { input: 0.15, output: 0.85, cacheRead: 0.015 }, + } + const p = MODEL_PRICING[model] ?? { input: 0, output: 0, cacheRead: 0 } + const regularInput = inputTokens - cachedInputTokens + return Math.round( + ((regularInput * p.input + cachedInputTokens * p.cacheRead + outputTokens * p.output) / 1_000_000) * 1_000_000 + ) / 1_000_000 +} + +function routeToProvider(params: { + provider: string + model: string +}): { aiModel: string; aiProvider: string } { + const { provider, model } = params + + switch (provider) { + case "google": + return { aiModel: model || "gemini-3-flash-preview", aiProvider: "google" } + case "openrouter": + return { aiModel: model || "openai/gpt-oss-120b:free", aiProvider: "openrouter" } + case "minimax": + return { aiModel: model || "minimax-m3", aiProvider: "minimax" } + case "nvidia": + return { aiModel: model || "minimaxai/minimax-m3", aiProvider: "nvidia" } + case "mergedev": + return { aiModel: model || "minimaxai/MiniMax-M2.7", aiProvider: "mergedev" } + case "orcarouter": + return { aiModel: model || "minimaxai/MiniMax-M2.7", aiProvider: "orcarouter" } + case "concentrateai": + return { aiModel: model || "minimaxai/MiniMax-M2.7", aiProvider: "concentrateai" } + default: + return { aiModel: model || "gemini-3-flash-preview", aiProvider: "google" } + } +} + +export async function createStreamText(opts: { + provider: string + model: string + apiKey?: string + baseUrl?: string + maxTokens?: number + temperature?: number + topP?: number + messages: ModelMessage[] + systemPrompt: string + tools?: Record + conversationId?: string +}): Promise { + const { allowed, remaining, queriesRemaining } = await checkDailyTokenBudget() + + if (!allowed) { + throw new Error( + `Daily token limit reached. You have ${remaining} tokens and ${queriesRemaining} queries remaining today. Please try again tomorrow.` + ) + } + + const { aiModel, aiProvider } = routeToProvider({ + provider: opts.provider, + model: opts.model, + }) + + let modelInstance: any + + switch (aiProvider) { + case "google": + modelInstance = google(aiModel) + break + case "openrouter": { + const or = createOpenRouter({ apiKey: opts.apiKey || process.env.OPENROUTER_API_KEY }) + modelInstance = or(aiModel) + break + } + case "minimax": + modelInstance = minimax(aiModel) + break + case "nvidia": + case "mergedev": + case "orcarouter": + case "concentrateai": { + const baseURL = opts.baseUrl || process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1" + const compat = createOpenAICompatible({ + name: aiProvider, + apiKey: opts.apiKey, + baseURL, + }) + modelInstance = compat(aiModel) + break + } + default: + modelInstance = google(aiModel) + } + + const result = streamText({ + model: modelInstance, + messages: opts.messages, + system: opts.systemPrompt, + maxOutputTokens: opts.maxTokens || 4096, + temperature: opts.temperature || 0.7, + topP: opts.topP || 0.95, + tools: opts.tools, + onFinish: async ({ usage }) => { + const cost = computeCost(aiModel, usage.inputTokens ?? 0, usage.outputTokens ?? 0, 0) + try { + await prisma.usageEvent.create({ + data: { + provider: aiProvider, + model: aiModel, + inputTokens: usage.inputTokens ?? 0, + outputTokens: usage.outputTokens ?? 0, + cachedInputTokens: 0, + costUsd: cost, + durationMs: 0, + }, + }) + } catch (e) { + console.error("[track-usage] Failed to record usage:", e) + } + }, + }) + + return result +} + +export { DAILY_TOKEN_LIMIT, DAILY_QUERY_LIMIT, buildSystemPrompt, computeCost } diff --git a/apps/api/src/routes/ai.ts b/apps/api/src/routes/ai.ts new file mode 100644 index 0000000..79231c1 --- /dev/null +++ b/apps/api/src/routes/ai.ts @@ -0,0 +1,135 @@ +import { Hono } from "hono" +import { z } from "zod" +import { stream } from "hono/streaming" +import { zValidator } from "@hono/zod-validator" +import { createStreamText, buildSystemPrompt } from "../providers" +import { authenticate } from "../middleware/auth" +import type { AppEnv } from "../types" +import type { ModelMessage } from "ai" + +const chatBodySchema = z.object({ + messages: z.array(z.object({ + role: z.enum(["system", "user", "assistant"]), + content: z.string(), + })), + provider: z.string().optional().default("google"), + model: z.string().optional().default("gemini-3-flash-preview"), + apiKey: z.string().optional(), + baseUrl: z.string().optional(), + maxTokens: z.number().optional(), + temperature: z.number().optional(), + topP: z.number().optional(), + conversationId: z.string().optional(), + profile: z.string().optional(), + projectDoc: z.string().optional(), +}) + +const generateObjectBodySchema = z.object({ + messages: z.array(z.object({ + role: z.enum(["system", "user", "assistant"]), + content: z.string(), + })), + provider: z.string().optional().default("google"), + model: z.string().optional().default("gemini-3-flash-preview"), + apiKey: z.string().optional(), + baseUrl: z.string().optional(), + temperature: z.number().optional(), + responseFormat: z.enum(["json", "text"]).optional().default("json"), +}) + +export const aiRoute = new Hono() + .post( + "/ai/chat", + authenticate, + zValidator("json", chatBodySchema), + async (c) => { + const user = c.get("user") + const body = c.req.valid("json") + + const coreMessages: ModelMessage[] = body.messages.map((m) => ({ + role: m.role as "system" | "user" | "assistant", + content: m.content, + })) + + const systemPrompt = buildSystemPrompt(body.profile, body.projectDoc) + + try { + const result = await createStreamText({ + provider: body.provider, + model: body.model, + apiKey: body.apiKey, + baseUrl: body.baseUrl, + maxTokens: body.maxTokens, + temperature: body.temperature, + topP: body.topP, + messages: coreMessages, + systemPrompt, + conversationId: body.conversationId, + }) + + c.header("Content-Type", "application/x-ndjson") + c.header("Cache-Control", "no-cache") + c.header("Connection", "keep-alive") + + return stream(c, async (s) => { + for await (const delta of result.textStream) { + const chunk = JSON.stringify({ type: "text-delta", textDelta: delta }) + "\n" + await s.write(chunk) + } + + const finishReason = await result.finishReason + const usage = await result.usage + + const finishChunk = JSON.stringify({ + type: "finish", + finishReason, + usage: { + promptTokens: usage.inputTokens ?? 0, + completionTokens: usage.outputTokens ?? 0, + totalTokens: (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0), + }, + }) + "\n" + await s.write(finishChunk) + }) + } catch (error: any) { + const errorMessage = error?.message || "Unknown error" + if (errorMessage.includes("Daily token limit")) { + return c.json({ error: errorMessage }, 429) + } + console.error("[AI Chat Error]", error) + return c.json({ error: errorMessage }, 500) + } + } + ) + .post( + "/ai/generate-object", + authenticate, + zValidator("json", generateObjectBodySchema), + async (c) => { + const user = c.get("user") + const body = c.req.valid("json") + + const coreMessages: ModelMessage[] = body.messages.map((m) => ({ + role: m.role as "system" | "user" | "assistant", + content: m.content, + })) + + try { + const result = await createStreamText({ + provider: body.provider, + model: body.model, + apiKey: body.apiKey, + baseUrl: body.baseUrl, + temperature: body.temperature, + messages: coreMessages, + systemPrompt: "You are a helpful assistant that generates structured JSON responses.", + }) + + const text = await result.text + return c.json({ object: text }) + } catch (error: any) { + console.error("[AI Generate Object Error]", error) + return c.json({ error: error?.message || "Unknown error" }, 500) + } + } + ) diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..ac051a4 --- /dev/null +++ b/apps/api/src/routes/auth.ts @@ -0,0 +1,20 @@ +import { Hono } from "hono" +import { authenticate } from "../middleware/auth" +import type { AppEnv } from "../types" + +export const authRoute = new Hono() + .get("/auth/me", authenticate, async (c) => { + const user = c.get("user") + return c.json({ + id: user.id, + name: user.name, + email: user.email, + image: user.image, + }) + }) + .post("/auth/sign-out", authenticate, async (c) => { + const session = c.get("session") + const { auth } = await import("../lib/auth") + await auth.api.signOut({ headers: new Headers({ Authorization: `Bearer ${session.token}` }) }) + return c.json({ success: true }) + }) diff --git a/apps/api/src/routes/health.ts b/apps/api/src/routes/health.ts new file mode 100644 index 0000000..04f7c1d --- /dev/null +++ b/apps/api/src/routes/health.ts @@ -0,0 +1,9 @@ +import { Hono } from "hono" + +export const healthRoute = new Hono().get("/health", (c) => { + return c.json({ + status: "ok", + timestamp: new Date().toISOString(), + service: "supercode-api", + }) +}) diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts new file mode 100644 index 0000000..06e98da --- /dev/null +++ b/apps/api/src/types.ts @@ -0,0 +1,24 @@ +import type { Hono } from "hono" + +type Bindings = {} +type Variables = { + user: { + id: string + name: string | null + email: string + image: string | null + } + session: { + id: string + token: string + expiresAt: Date + userId: string + } +} + +export type AppEnv = { + Bindings: Bindings + Variables: Variables +} + +export type App = Hono diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..12ebd01 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "paths": { + "@super/db-terminal": ["../../packages/db-terminal/index.ts"], + "@super/db-terminal/*": ["../../packages/db-terminal/*"] + } + }, + "include": ["src"] +} diff --git a/bun.lock b/bun.lock index 53f0fec..30c5f7a 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,22 @@ "apps/api": { "name": "api", "version": "0.1.0", + "dependencies": { + "@ai-sdk/google": "^3.0.80", + "@ai-sdk/openai-compatible": "^2.0.48", + "@hono/zod-validator": "^0.5.0", + "@openrouter/ai-sdk-provider": "^2.9.0", + "@super/db-terminal": "workspace:*", + "ai": "^7.0.37", + "better-auth": "^1.6.23", + "hono": "^4.7.0", + "vercel-minimax-ai-provider": "^0.0.2", + "zod": "^3.25.2", + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "typescript": "^5.7.0", + }, }, "apps/docs": { "name": "docs", @@ -352,6 +368,12 @@ "packages/sdk": { "name": "@super/sdk", "version": "0.1.0", + "dependencies": { + "zod": "^3.25.2", + }, + "devDependencies": { + "typescript": "^5.7.0", + }, }, "packages/secrets": { "name": "@super/secrets", @@ -698,6 +720,8 @@ "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], + "@hono/zod-validator": ["@hono/zod-validator@0.5.0", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -2612,7 +2636,7 @@ "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], - "hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="], + "hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], @@ -3968,8 +3992,6 @@ "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], - "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], @@ -4212,8 +4234,6 @@ "@prisma/adapter-pg/pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="], - "@prisma/dev/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], - "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="], "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="], @@ -4492,6 +4512,12 @@ "ansi-align/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=="], + "api/@ai-sdk/google": ["@ai-sdk/google@3.0.80", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5ORbm/yFUPO0MEvZsxBMN0cdKw2+lwU/wVn5KN3KF8Dmk1LughuDuUohMh/7iU/XFTiyB0OvmTW/tdV/J7O9zg=="], + + "api/ai": ["ai@7.0.37", "", { "dependencies": { "@ai-sdk/gateway": "4.0.28", "@ai-sdk/provider": "4.0.3", "@ai-sdk/provider-utils": "5.0.12" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-stF+SEQJgKY3Qfe3FwNzqUrehHviOp2l7LemoI8YMOa0Zk5PxKsRNgUk3cHXz0RAue9RRyCAis2fz6LSCP6EKw=="], + + "api/better-auth": ["better-auth@1.6.23", "", { "dependencies": { "@better-auth/core": "1.6.23", "@better-auth/drizzle-adapter": "1.6.23", "@better-auth/kysely-adapter": "1.6.23", "@better-auth/memory-adapter": "1.6.23", "@better-auth/mongo-adapter": "1.6.23", "@better-auth/prisma-adapter": "1.6.23", "@better-auth/telemetry": "1.6.23", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.7", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ=="], + "better-auth/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "better-auth/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], @@ -5260,6 +5286,42 @@ "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "api/@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "api/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + + "api/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.28", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@ai-sdk/provider-utils": "5.0.12", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ee9TsNO3mkgHDWmTmJ8Fvltr6PFh52zhrV2+FaKJY3F3iu7kWZi5tCRJCR1HVhhlpj6lHniUb28nLQdPHkA/1Q=="], + + "api/ai/@ai-sdk/provider": ["@ai-sdk/provider@4.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw=="], + + "api/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.12", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA=="], + + "api/better-auth/@better-auth/core": ["@better-auth/core@1.6.23", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.7", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ=="], + + "api/better-auth/@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ=="], + + "api/better-auth/@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg=="], + + "api/better-auth/@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2" } }, "sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ=="], + + "api/better-auth/@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA=="], + + "api/better-auth/@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ=="], + + "api/better-auth/@better-auth/telemetry": ["@better-auth/telemetry@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA=="], + + "api/better-auth/@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], + + "api/better-auth/@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], + + "api/better-auth/better-call": ["better-call@1.3.7", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w=="], + + "api/better-auth/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "api/better-auth/kysely": ["kysely@0.29.2", "", {}, "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg=="], + + "api/better-auth/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -5708,6 +5770,14 @@ "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "api/@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + + "api/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + + "api/ai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + + "api/better-auth/better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "eve/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], diff --git a/packages/sdk/client.ts b/packages/sdk/client.ts new file mode 100644 index 0000000..343a2db --- /dev/null +++ b/packages/sdk/client.ts @@ -0,0 +1,161 @@ +import type { + ChatRequest, + GenerateObjectRequest, + ChatResponse, + GenerateObjectResponse, + HealthResponse, + UserResponse, + StreamChunk, + SupercodeClientConfig, +} from "./types" +import { + chatRequestSchema, + generateObjectRequestSchema, + healthResponseSchema, + userResponseSchema, + streamChunkSchema, +} from "./schemas" + +export class SupercodeClient { + private baseUrl: string + private token: string + + constructor(config: SupercodeClientConfig) { + this.baseUrl = config.baseUrl.replace(/\/$/, "") + this.token = config.token + } + + private get headers() { + return { + "Content-Type": "application/json", + Authorization: `Bearer ${this.token}`, + } + } + + async health(): Promise { + const res = await fetch(`${this.baseUrl}/api/health`) + const data = await res.json() + return healthResponseSchema.parse(data) + } + + async me(): Promise { + const res = await fetch(`${this.baseUrl}/api/auth/me`, { + headers: this.headers, + }) + if (!res.ok) { + const error = await res.json() + throw new Error(error.error || "Failed to get user") + } + const data = await res.json() + return userResponseSchema.parse(data) + } + + async chat(request: ChatRequest): Promise { + const validated = chatRequestSchema.parse(request) + const res = await fetch(`${this.baseUrl}/api/ai/chat`, { + method: "POST", + headers: this.headers, + body: JSON.stringify(validated), + }) + + if (!res.ok) { + const error = await res.json() + throw new Error(error.error || "Chat request failed") + } + + let text = "" + let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 } + let finishReason = "" + + const reader = res.body?.getReader() + if (!reader) throw new Error("No response body") + + const decoder = new TextDecoder() + let buffer = "" + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() || "" + + for (const line of lines) { + if (!line.trim()) continue + try { + const chunk = JSON.parse(line) + const parsed = streamChunkSchema.parse(chunk) + + if (parsed.type === "text-delta") { + text += parsed.textDelta + } else if (parsed.type === "finish") { + finishReason = parsed.finishReason + usage = parsed.usage + } + } catch { + // Skip invalid JSON lines + } + } + } + + return { text, usage, finishReason } + } + + async *chatStream(request: ChatRequest): AsyncGenerator { + const validated = chatRequestSchema.parse(request) + const res = await fetch(`${this.baseUrl}/api/ai/chat`, { + method: "POST", + headers: this.headers, + body: JSON.stringify(validated), + }) + + if (!res.ok) { + const error = await res.json() + throw new Error(error.error || "Chat request failed") + } + + const reader = res.body?.getReader() + if (!reader) throw new Error("No response body") + + const decoder = new TextDecoder() + let buffer = "" + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() || "" + + for (const line of lines) { + if (!line.trim()) continue + try { + const chunk = JSON.parse(line) + const parsed = streamChunkSchema.parse(chunk) + yield parsed + } catch { + // Skip invalid JSON lines + } + } + } + } + + async generateObject(request: GenerateObjectRequest): Promise { + const validated = generateObjectRequestSchema.parse(request) + const res = await fetch(`${this.baseUrl}/api/ai/generate-object`, { + method: "POST", + headers: this.headers, + body: JSON.stringify(validated), + }) + + if (!res.ok) { + const error = await res.json() + throw new Error(error.error || "Generate object request failed") + } + + const data = await res.json() + return { object: data.object } + } +} diff --git a/packages/sdk/index.ts b/packages/sdk/index.ts index e69de29..587d7d5 100644 --- a/packages/sdk/index.ts +++ b/packages/sdk/index.ts @@ -0,0 +1,30 @@ +export { SupercodeClient } from "./client" +export type { + ChatRequest, + GenerateObjectRequest, + ChatResponse, + GenerateObjectResponse, + HealthResponse, + UserResponse, + StreamChunk, + ChatChunk, + FinishChunk, + Usage, + Message, + MessageRole, + Provider, + SupercodeClientConfig, +} from "./types" +export { + chatRequestSchema, + generateObjectRequestSchema, + healthResponseSchema, + userResponseSchema, + streamChunkSchema, + chatChunkSchema, + finishChunkSchema, + usageSchema, + messageSchema, + providerSchema, + apiErrorSchema, +} from "./schemas" diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c3f8b8b..33d9b58 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -7,6 +7,12 @@ "scripts": { "build": "echo 'sdk build pending'", "lint": "echo 'sdk lint pending'", - "typecheck": "echo 'sdk typecheck pending'" + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "zod": "^3.25.2" + }, + "devDependencies": { + "typescript": "^5.7.0" } } diff --git a/packages/sdk/schemas.ts b/packages/sdk/schemas.ts new file mode 100644 index 0000000..d4b3236 --- /dev/null +++ b/packages/sdk/schemas.ts @@ -0,0 +1,79 @@ +import { z } from "zod" + +export const messageSchema = z.object({ + role: z.enum(["system", "user", "assistant"]), + content: z.string(), +}) + +export const providerSchema = z.enum([ + "google", + "openrouter", + "minimax", + "nvidia", + "mergedev", + "orcarouter", + "concentrateai", +]) + +export const chatRequestSchema = z.object({ + messages: z.array(messageSchema), + provider: providerSchema.optional(), + model: z.string().optional(), + apiKey: z.string().optional(), + baseUrl: z.string().optional(), + maxTokens: z.number().optional(), + temperature: z.number().optional(), + topP: z.number().optional(), + conversationId: z.string().optional(), + profile: z.string().optional(), + projectDoc: z.string().optional(), +}) + +export const generateObjectRequestSchema = z.object({ + messages: z.array(messageSchema), + provider: providerSchema.optional(), + model: z.string().optional(), + apiKey: z.string().optional(), + baseUrl: z.string().optional(), + temperature: z.number().optional(), + responseFormat: z.enum(["json", "text"]).optional(), +}) + +export const usageSchema = z.object({ + promptTokens: z.number(), + completionTokens: z.number(), + totalTokens: z.number(), +}) + +export const chatChunkSchema = z.object({ + type: z.literal("text-delta"), + textDelta: z.string(), +}) + +export const finishChunkSchema = z.object({ + type: z.literal("finish"), + finishReason: z.string(), + usage: usageSchema, +}) + +export const streamChunkSchema = z.discriminatedUnion("type", [ + chatChunkSchema, + finishChunkSchema, +]) + +export const healthResponseSchema = z.object({ + status: z.literal("ok"), + timestamp: z.string(), + service: z.string(), +}) + +export const userResponseSchema = z.object({ + id: z.string(), + name: z.string().nullable(), + email: z.string(), + image: z.string().nullable(), +}) + +export const apiErrorSchema = z.object({ + error: z.string(), +}) diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100644 index 0000000..efdf22a --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "include": ["*.ts"] +} diff --git a/packages/sdk/types.ts b/packages/sdk/types.ts new file mode 100644 index 0000000..7bdfe59 --- /dev/null +++ b/packages/sdk/types.ts @@ -0,0 +1,90 @@ +export type MessageRole = "system" | "user" | "assistant" + +export interface Message { + role: MessageRole + content: string +} + +export type Provider = + | "google" + | "openrouter" + | "minimax" + | "nvidia" + | "mergedev" + | "orcarouter" + | "concentrateai" + +export interface ChatRequest { + messages: Message[] + provider?: Provider + model?: string + apiKey?: string + baseUrl?: string + maxTokens?: number + temperature?: number + topP?: number + conversationId?: string + profile?: string + projectDoc?: string +} + +export interface GenerateObjectRequest { + messages: Message[] + provider?: Provider + model?: string + apiKey?: string + baseUrl?: string + temperature?: number + responseFormat?: "json" | "text" +} + +export interface Usage { + promptTokens: number + completionTokens: number + totalTokens: number +} + +export interface ChatChunk { + type: "text-delta" + textDelta: string +} + +export interface FinishChunk { + type: "finish" + finishReason: string + usage: Usage +} + +export type StreamChunk = ChatChunk | FinishChunk + +export interface ChatResponse { + text: string + usage: Usage + finishReason: string +} + +export interface GenerateObjectResponse { + object: string +} + +export interface HealthResponse { + status: "ok" + timestamp: string + service: string +} + +export interface UserResponse { + id: string + name: string | null + email: string + image: string | null +} + +export interface ApiError { + error: string +} + +export interface SupercodeClientConfig { + baseUrl: string + token: string +}