openpencil/server/api/ai/connect-agent.ts
Fini 2e61c5e640 feat(ai): add Setup Agents & MCP dialog with multi-provider support
- Add agent settings dialog to connect local Claude Code and Codex CLI
- Connect button fetches real models from Agent SDK / Codex models cache
- Provider logos (Claude, OpenAI) as React icon components in top-bar and dialog
- Model selector in AI chat panel grouped by provider with logos and Best badge
- MCP CLI integration toggles (Claude Code, Codex, Gemini, OpenCode, Kiro)
- Persist agent settings to localStorage with hydrate on mount
- Redesign AI chat panel: header, empty state pills, input area, color layering
- Add shadcn Switch component, agent-settings types and Zustand store
2026-02-19 03:15:36 +08:00

140 lines
4 KiB
TypeScript

import { defineEventHandler, readBody, setResponseHeaders } from 'h3'
import type { GroupedModel } from '../../../src/types/agent-settings'
interface ConnectBody {
agent: 'claude-code' | 'codex-cli'
}
interface ConnectResult {
connected: boolean
models: GroupedModel[]
error?: string
}
/**
* POST /api/ai/connect-agent
* Actively connects to a local CLI tool and fetches its supported models.
*/
export default defineEventHandler(async (event) => {
const body = await readBody<ConnectBody>(event)
setResponseHeaders(event, { 'Content-Type': 'application/json' })
if (!body?.agent) {
return { connected: false, models: [], error: 'Missing agent field' } satisfies ConnectResult
}
if (body.agent === 'claude-code') {
return connectClaudeCode()
}
if (body.agent === 'codex-cli') {
return connectCodexCli()
}
return { connected: false, models: [], error: `Unknown agent: ${body.agent}` } satisfies ConnectResult
})
/** Connect to Claude Code via Agent SDK and fetch real supported models */
async function connectClaudeCode(): Promise<ConnectResult> {
try {
const { query } = await import('@anthropic-ai/claude-agent-sdk')
const env = { ...process.env } as Record<string, string | undefined>
delete env.CLAUDECODE
const q = query({
prompt: '',
options: {
model: 'claude-sonnet-4-6',
maxTurns: 1,
tools: [],
permissionMode: 'plan',
persistSession: false,
env,
},
})
const raw = await q.supportedModels()
q.close()
const models: GroupedModel[] = raw.map((m) => ({
value: m.value,
displayName: m.displayName,
description: m.description,
provider: 'anthropic' as const,
}))
return { connected: true, models }
} catch (error) {
const msg = error instanceof Error ? error.message : 'Failed to connect'
return { connected: false, models: [], error: msg }
}
}
/** Connect to Codex CLI and fetch its supported models from the local cache */
async function connectCodexCli(): Promise<ConnectResult> {
try {
const { execSync } = await import('node:child_process')
const { readFile } = await import('node:fs/promises')
const { homedir } = await import('node:os')
const { join } = await import('node:path')
// Check if codex binary exists
const which = execSync('which codex 2>/dev/null || echo ""', {
encoding: 'utf-8',
timeout: 5000,
}).trim()
if (!which) {
return { connected: false, models: [], error: 'codex command not found. Install Codex CLI first.' }
}
// Verify codex is responsive
try {
execSync('codex --version 2>&1', { encoding: 'utf-8', timeout: 5000 })
} catch {
return { connected: false, models: [], error: 'Codex CLI not responding' }
}
// Read models from Codex CLI's local models cache
let models: GroupedModel[] = []
const cachePath = join(homedir(), '.codex', 'models_cache.json')
try {
const raw = await readFile(cachePath, 'utf-8')
const cache = JSON.parse(raw) as {
models?: Array<{
slug: string
display_name: string
description: string
visibility: string
priority: number
}>
}
if (cache.models && Array.isArray(cache.models)) {
models = cache.models
.filter((m) => m.visibility === 'list')
.sort((a, b) => (a.priority ?? 999) - (b.priority ?? 999))
.map((m) => ({
value: m.slug,
displayName: m.display_name,
description: m.description ?? '',
provider: 'openai' as const,
}))
}
} catch {
// Cache file not found or unreadable
}
if (models.length === 0) {
return { connected: false, models: [], error: 'No models found. Try running codex once to populate the model cache.' }
}
return { connected: true, models }
} catch (error) {
const msg = error instanceof Error ? error.message : 'Failed to connect'
return { connected: false, models: [], error: msg }
}
}