diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..685fa48 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +.git +.data +.next +.env +*.md +.github +ops +scripts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0beefce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:20-slim AS base +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app + +FROM base AS deps +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +FROM base AS build +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm build + +FROM node:20-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +COPY --from=build /app/.next/standalone ./ +COPY --from=build /app/.next/static ./.next/static +COPY --from=build /app/public ./public +USER nextjs +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3c8b968 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + mission-control: + build: . + ports: + - "3000:3000" + env_file: .env + volumes: + - mc-data:/app/.data + restart: unless-stopped + +volumes: + mc-data: diff --git a/next.config.js b/next.config.js index e6dc2ed..e24a943 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'standalone', turbopack: {}, // Security headers diff --git a/src/app/api/agents/route.ts b/src/app/api/agents/route.ts index a772c62..168b591 100644 --- a/src/app/api/agents/route.ts +++ b/src/app/api/agents/route.ts @@ -5,6 +5,7 @@ import { getTemplate, buildAgentConfig } from '@/lib/agent-templates'; import { writeAgentToConfig } from '@/lib/agent-sync'; import { logAuditEvent } from '@/lib/db'; import { getUserFromRequest, requireRole } from '@/lib/auth'; +import { mutationLimiter } from '@/lib/rate-limit'; import { logger } from '@/lib/logger'; import { validateBody, createAgentSchema } from '@/lib/validation'; @@ -109,6 +110,9 @@ export async function POST(request: NextRequest) { const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const validated = await validateBody(request, createAgentSchema); @@ -134,11 +138,11 @@ export async function POST(request: NextRequest) { const tpl = getTemplate(template); if (tpl) { const builtConfig = buildAgentConfig(tpl, (gateway_config || {}) as any); - finalConfig = { ...builtConfig, ...config }; + finalConfig = { ...builtConfig, ...finalConfig }; if (!finalRole) finalRole = tpl.config.identity?.theme || tpl.type; } } else if (gateway_config) { - finalConfig = { ...config, ...gateway_config }; + finalConfig = { ...finalConfig, ...(gateway_config as Record) }; } if (!name || !finalRole) { @@ -247,10 +251,13 @@ export async function PUT(request: NextRequest) { const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const body = await request.json(); - + // Handle single agent update or bulk updates if (body.name) { // Single agent update diff --git a/src/app/api/alerts/route.ts b/src/app/api/alerts/route.ts index 6cb116e..9d56f0f 100644 --- a/src/app/api/alerts/route.ts +++ b/src/app/api/alerts/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { requireRole } from '@/lib/auth' import { getDatabase } from '@/lib/db' +import { mutationLimiter } from '@/lib/rate-limit' import { createAlertSchema } from '@/lib/validation' interface AlertRule { @@ -45,6 +46,9 @@ export async function POST(request: NextRequest) { const auth = requireRole(request, 'operator') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = mutationLimiter(request) + if (rateCheck) return rateCheck + const db = getDatabase() const body = await request.json() @@ -103,6 +107,9 @@ export async function PUT(request: NextRequest) { const auth = requireRole(request, 'operator') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = mutationLimiter(request) + if (rateCheck) return rateCheck + const db = getDatabase() const body = await request.json() const { id, ...updates } = body @@ -141,6 +148,9 @@ export async function DELETE(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = mutationLimiter(request) + if (rateCheck) return rateCheck + const db = getDatabase() const body = await request.json() const { id } = body diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index a4e7805..2da7472 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -2,28 +2,13 @@ import { NextResponse } from 'next/server' import { authenticateUser, createSession } from '@/lib/auth' import { logAuditEvent } from '@/lib/db' import { getMcSessionCookieOptions } from '@/lib/session-cookie' +import { loginLimiter } from '@/lib/rate-limit' import { logger } from '@/lib/logger' -// Rate limiting: 5 attempts per minute per IP -const loginAttempts = new Map() - -function checkRateLimit(ip: string): boolean { - const now = Date.now() - const entry = loginAttempts.get(ip) - if (!entry || now > entry.resetAt) { - loginAttempts.set(ip, { count: 1, resetAt: now + 60_000 }) - return true - } - entry.count++ - return entry.count <= 5 -} - export async function POST(request: Request) { try { - const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown' - if (!checkRateLimit(ip)) { - return NextResponse.json({ error: 'Too many login attempts. Try again in a minute.' }, { status: 429 }) - } + const rateCheck = loginLimiter(request) + if (rateCheck) return rateCheck const { username, password } = await request.json() diff --git a/src/app/api/export/route.ts b/src/app/api/export/route.ts index e927142..a495ee7 100644 --- a/src/app/api/export/route.ts +++ b/src/app/api/export/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { requireRole } from '@/lib/auth' import { getDatabase, logAuditEvent } from '@/lib/db' +import { heavyLimiter } from '@/lib/rate-limit' /** * GET /api/export?type=audit|tasks|activities|pipelines&format=csv|json&since=UNIX&until=UNIX @@ -10,6 +11,9 @@ export async function GET(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = heavyLimiter(request) + if (rateCheck) return rateCheck + const { searchParams } = new URL(request.url) const type = searchParams.get('type') const format = searchParams.get('format') || 'csv' diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts index f604472..639cf59 100644 --- a/src/app/api/notifications/route.ts +++ b/src/app/api/notifications/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase, Notification } from '@/lib/db'; import { requireRole } from '@/lib/auth'; +import { mutationLimiter } from '@/lib/rate-limit'; /** * GET /api/notifications - Get notifications for a specific recipient @@ -138,6 +139,9 @@ export async function PUT(request: NextRequest) { const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const body = await request.json(); @@ -193,6 +197,9 @@ export async function DELETE(request: NextRequest) { const auth = requireRole(request, 'admin'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const body = await request.json(); @@ -244,6 +251,9 @@ export async function POST(request: NextRequest) { const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const body = await request.json(); diff --git a/src/app/api/sessions/[id]/control/route.ts b/src/app/api/sessions/[id]/control/route.ts new file mode 100644 index 0000000..573c9d5 --- /dev/null +++ b/src/app/api/sessions/[id]/control/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireRole } from '@/lib/auth' +import { runClawdbot } from '@/lib/command' +import { db_helpers } from '@/lib/db' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const auth = requireRole(request, 'operator') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + + try { + const { id } = await params + const { action } = await request.json() + + if (!['monitor', 'pause', 'terminate'].includes(action)) { + return NextResponse.json( + { error: 'Invalid action. Must be: monitor, pause, terminate' }, + { status: 400 } + ) + } + + let result + if (action === 'terminate') { + result = await runClawdbot( + ['-c', `sessions_kill("${id}")`], + { timeoutMs: 10000 } + ) + } else { + const message = action === 'monitor' + ? JSON.stringify({ type: 'control', action: 'monitor' }) + : JSON.stringify({ type: 'control', action: 'pause' }) + result = await runClawdbot( + ['-c', `sessions_send("${id}", ${JSON.stringify(message)})`], + { timeoutMs: 10000 } + ) + } + + db_helpers.logActivity( + 'session_control', + 'session', + 0, + auth.user.username, + `Session ${action}: ${id}`, + { session_key: id, action } + ) + + return NextResponse.json({ + success: true, + action, + session: id, + stdout: result.stdout.trim(), + }) + } catch (error: any) { + return NextResponse.json( + { error: error.message || 'Session control failed' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/settings/route.ts b/src/app/api/settings/route.ts index ceb00a1..6bdb5f3 100644 --- a/src/app/api/settings/route.ts +++ b/src/app/api/settings/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { requireRole } from '@/lib/auth' import { getDatabase, logAuditEvent } from '@/lib/db' import { config } from '@/lib/config' +import { mutationLimiter } from '@/lib/rate-limit' interface SettingRow { key: string @@ -101,6 +102,9 @@ export async function PUT(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = mutationLimiter(request) + if (rateCheck) return rateCheck + const body = await request.json().catch(() => null) if (!body?.settings || typeof body.settings !== 'object') { return NextResponse.json({ error: 'settings object required' }, { status: 400 }) @@ -157,6 +161,9 @@ export async function DELETE(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = mutationLimiter(request) + if (rateCheck) return rateCheck + let body: any try { body = await request.json() } catch { return NextResponse.json({ error: 'Request body required' }, { status: 400 }) } const key = body.key diff --git a/src/app/api/spawn/route.ts b/src/app/api/spawn/route.ts index 395e610..e62554e 100644 --- a/src/app/api/spawn/route.ts +++ b/src/app/api/spawn/route.ts @@ -4,12 +4,16 @@ import { requireRole } from '@/lib/auth' import { config } from '@/lib/config' import { readdir, readFile, stat } from 'fs/promises' import { join } from 'path' +import { heavyLimiter } from '@/lib/rate-limit' import { logger } from '@/lib/logger' export async function POST(request: NextRequest) { const auth = requireRole(request, 'operator') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = heavyLimiter(request) + if (rateCheck) return rateCheck + try { const { task, model, label, timeoutSeconds } = await request.json() diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts index b540f5f..d17af95 100644 --- a/src/app/api/status/route.ts +++ b/src/app/api/status/route.ts @@ -6,6 +6,7 @@ import { config } from '@/lib/config' import { getDatabase } from '@/lib/db' import { getAllGatewaySessions, getAgentLiveStatuses } from '@/lib/sessions' import { requireRole } from '@/lib/auth' +import { MODEL_CATALOG } from '@/lib/models' export async function GET(request: NextRequest) { const auth = requireRole(request, 'viewer') @@ -340,17 +341,8 @@ async function getGatewayStatus() { async function getAvailableModels() { // This would typically query the gateway or config files - // For now, return the models from AGENTS.md - const models = [ - { alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 }, - { alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 }, - { alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 }, - { alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 }, - { alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 }, - { alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 }, - { alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 }, - { alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 }, - ] + // Model catalog is the single source of truth + const models = [...MODEL_CATALOG] try { // Check which Ollama models are available locally diff --git a/src/app/api/tasks/[id]/route.ts b/src/app/api/tasks/[id]/route.ts index e3bda02..c38b859 100644 --- a/src/app/api/tasks/[id]/route.ts +++ b/src/app/api/tasks/[id]/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase, Task, db_helpers } from '@/lib/db'; import { eventBus } from '@/lib/event-bus'; import { getUserFromRequest, requireRole } from '@/lib/auth'; +import { mutationLimiter } from '@/lib/rate-limit'; import { logger } from '@/lib/logger'; import { validateBody, updateTaskSchema } from '@/lib/validation'; @@ -65,6 +66,9 @@ export async function PUT( const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const resolvedParams = await params; @@ -259,6 +263,9 @@ export async function DELETE( const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const resolvedParams = await params; diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index e7ec5df..ceaedf2 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase, Task, db_helpers } from '@/lib/db'; import { eventBus } from '@/lib/event-bus'; import { requireRole } from '@/lib/auth'; +import { mutationLimiter } from '@/lib/rate-limit'; import { logger } from '@/lib/logger'; import { validateBody, createTaskSchema } from '@/lib/validation'; @@ -97,6 +98,9 @@ export async function POST(request: NextRequest) { const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const validated = await validateBody(request, createTaskSchema); @@ -199,6 +203,9 @@ export async function PUT(request: NextRequest) { const auth = requireRole(request, 'operator'); if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + const rateCheck = mutationLimiter(request); + if (rateCheck) return rateCheck; + try { const db = getDatabase(); const { tasks } = await request.json(); diff --git a/src/app/api/webhooks/route.ts b/src/app/api/webhooks/route.ts index c6ca8d7..465d3e5 100644 --- a/src/app/api/webhooks/route.ts +++ b/src/app/api/webhooks/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getDatabase } from '@/lib/db' import { requireRole } from '@/lib/auth' import { randomBytes, createHmac } from 'crypto' +import { mutationLimiter } from '@/lib/rate-limit' import { logger } from '@/lib/logger' import { validateBody, createWebhookSchema } from '@/lib/validation' @@ -45,6 +46,9 @@ export async function POST(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = mutationLimiter(request) + if (rateCheck) return rateCheck + try { const db = getDatabase() const validated = await validateBody(request, createWebhookSchema) @@ -82,6 +86,9 @@ export async function PUT(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = mutationLimiter(request) + if (rateCheck) return rateCheck + try { const db = getDatabase() const body = await request.json() @@ -137,6 +144,9 @@ export async function DELETE(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const rateCheck = mutationLimiter(request) + if (rateCheck) return rateCheck + try { const db = getDatabase() let body: any diff --git a/src/components/panels/session-details-panel.tsx b/src/components/panels/session-details-panel.tsx index 8e129ee..4de86dd 100644 --- a/src/components/panels/session-details-panel.tsx +++ b/src/components/panels/session-details-panel.tsx @@ -26,6 +26,7 @@ export function SessionDetailsPanel() { useSmartPoll(loadSessions, 60000, { pauseWhenConnected: true }) + const [controllingSession, setControllingSession] = useState(null) const [sessionFilter, setSessionFilter] = useState<'all' | 'active' | 'idle'>('all') const [sortBy, setSortBy] = useState<'age' | 'tokens' | 'model'>('age') const [expandedSession, setExpandedSession] = useState(null) @@ -323,36 +324,80 @@ export function SessionDetailsPanel() { {/* Actions */}
- - + +
diff --git a/src/lib/models.ts b/src/lib/models.ts new file mode 100644 index 0000000..ca10a79 --- /dev/null +++ b/src/lib/models.ts @@ -0,0 +1,30 @@ +export interface ModelConfig { + alias: string + name: string + provider: string + description: string + costPer1k: number +} + +export const MODEL_CATALOG: ModelConfig[] = [ + { alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 }, + { alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 }, + { alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 }, + { alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 }, + { alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 }, + { alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 }, + { alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 }, + { alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 }, +] + +export function getModelByAlias(alias: string): ModelConfig | undefined { + return MODEL_CATALOG.find(m => m.alias === alias) +} + +export function getModelByName(name: string): ModelConfig | undefined { + return MODEL_CATALOG.find(m => m.name === name) +} + +export function getAllModels(): ModelConfig[] { + return [...MODEL_CATALOG] +} diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts new file mode 100644 index 0000000..2fdb7b8 --- /dev/null +++ b/src/lib/rate-limit.ts @@ -0,0 +1,64 @@ +import { NextResponse } from 'next/server' + +interface RateLimitEntry { + count: number + resetAt: number +} + +interface RateLimiterOptions { + windowMs: number + maxRequests: number + message?: string +} + +export function createRateLimiter(options: RateLimiterOptions) { + const store = new Map() + + // Periodic cleanup every 60s + const cleanupInterval = setInterval(() => { + const now = Date.now() + for (const [key, entry] of store) { + if (now > entry.resetAt) store.delete(key) + } + }, 60_000) + // Don't prevent process exit + if (cleanupInterval.unref) cleanupInterval.unref() + + return function checkRateLimit(request: Request): NextResponse | null { + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown' + const now = Date.now() + const entry = store.get(ip) + + if (!entry || now > entry.resetAt) { + store.set(ip, { count: 1, resetAt: now + options.windowMs }) + return null + } + + entry.count++ + if (entry.count > options.maxRequests) { + return NextResponse.json( + { error: options.message || 'Too many requests. Please try again later.' }, + { status: 429 } + ) + } + + return null + } +} + +export const loginLimiter = createRateLimiter({ + windowMs: 60_000, + maxRequests: 5, + message: 'Too many login attempts. Try again in a minute.', +}) + +export const mutationLimiter = createRateLimiter({ + windowMs: 60_000, + maxRequests: 60, +}) + +export const heavyLimiter = createRateLimiter({ + windowMs: 60_000, + maxRequests: 10, + message: 'Too many requests for this resource. Please try again later.', +}) diff --git a/src/lib/validation.ts b/src/lib/validation.ts index bb09c77..ac8c93a 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -36,8 +36,8 @@ export const createTaskSchema = z.object({ due_date: z.number().optional(), estimated_hours: z.number().min(0).optional(), actual_hours: z.number().min(0).optional(), - tags: z.array(z.string()).default([]), - metadata: z.record(z.string(), z.unknown()).default({}), + tags: z.array(z.string()).default([] as string[]), + metadata: z.record(z.string(), z.unknown()).default({} as Record), }) export const updateTaskSchema = createTaskSchema.partial() @@ -48,7 +48,7 @@ export const createAgentSchema = z.object({ session_key: z.string().max(200).optional(), soul_content: z.string().max(50000).optional(), status: z.enum(['online', 'offline', 'busy', 'idle', 'error']).default('offline'), - config: z.record(z.string(), z.unknown()).default({}), + config: z.record(z.string(), z.unknown()).default({} as Record), template: z.string().max(100).optional(), gateway_config: z.record(z.string(), z.unknown()).optional(), write_to_gateway: z.boolean().optional(), diff --git a/src/store/index.ts b/src/store/index.ts index e9861ad..ed54c18 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' +import { MODEL_CATALOG } from '@/lib/models' // Enhanced types for Mission Control export interface Session { @@ -523,16 +524,7 @@ export const useMissionControl = create()( }, // Model Configuration - availableModels: [ - { alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 }, - { alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 }, - { alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 }, - { alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 }, - { alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 }, - { alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 }, - { alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 }, - { alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 }, - ], + availableModels: [...MODEL_CATALOG], setAvailableModels: (models) => set({ availableModels: models }), // Auth