"use server"; import { NextResponse } from "next/server"; import fs from "fs/promises"; import path from "path"; import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; import { ensureCliConfigWriteAllowed, getCliPrimaryConfigPath, getCliRuntimeStatus, } from "@/shared/services/cliRuntime"; import { createBackup } from "@/shared/services/backupService"; import { normalizeClaudeBaseUrl } from "@/shared/services/claudeCliConfig"; import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState"; import { cliSettingsEnvSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { getApiKeyById } from "@/lib/localDb"; import { readJsoncConfig } from "../_lib/jsoncConfig"; // Get claude settings path based on OS const getClaudeSettingsPath = () => getCliPrimaryConfigPath("claude"); // Read current settings. // Ported from upstream decolua/9router@6c10edf8: tolerate JSONC (trailing // commas) and return null on any parse error so the dashboard renders // "installed but not configured" instead of a 500 misread as "not installed". const readSettings = async () => { const settingsPath = getClaudeSettingsPath(); return readJsoncConfig(settingsPath); }; // GET - Check claude CLI and read current settings export async function GET(request: Request) { const authError = await requireCliToolsAuth(request); if (authError) return authError; try { const runtime = await getCliRuntimeStatus("claude"); if (!runtime.installed || !runtime.runnable) { return NextResponse.json({ installed: runtime.installed, runnable: runtime.runnable, command: runtime.command, commandPath: runtime.commandPath, runtimeMode: runtime.runtimeMode, reason: runtime.reason, settings: null, message: runtime.installed && !runtime.runnable ? "Claude CLI is installed but not runnable" : "Claude CLI is not installed", }); } const settings = await readSettings(); const hasOmniRoute = !!settings?.env?.ANTHROPIC_BASE_URL; return NextResponse.json({ installed: runtime.installed, runnable: runtime.runnable, command: runtime.command, commandPath: runtime.commandPath, runtimeMode: runtime.runtimeMode, reason: runtime.reason, settings: settings, hasOmniRoute: hasOmniRoute, settingsPath: getClaudeSettingsPath(), }); } catch (error) { console.log("Error checking claude settings:", error); return NextResponse.json({ error: "Failed to check claude settings" }, { status: 500 }); } } // POST - Backup old fields and write new settings export async function POST(request: Request) { const authError = await requireCliToolsAuth(request); if (authError) return authError; let rawBody; try { rawBody = await request.json(); } catch { return NextResponse.json( { error: { message: "Invalid request", details: [{ field: "body", message: "Invalid JSON body" }], }, }, { status: 400 } ); } try { const writeGuard = ensureCliConfigWriteAllowed(); if (writeGuard) { return NextResponse.json({ error: writeGuard }, { status: 403 }); } // (#523/#526) Extract keyId BEFORE validation — Zod strips unknown fields! // The /api/keys list endpoint returns masked key strings — sending those to // disk would save an unusable half-hidden token. Resolving by ID guarantees // we always write the full key value to the config file. const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; const validation = validateBody(cliSettingsEnvSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const { env } = validation.data; // Resolve the real API key from DB by ID if (keyId) { try { const keyRecord = await getApiKeyById(keyId); if (keyRecord?.key) { env.ANTHROPIC_AUTH_TOKEN = keyRecord.key as string; } } catch { // Non-critical: fall back to whatever value was already provided in env. } } const settingsPath = getClaudeSettingsPath(); const claudeDir = path.dirname(settingsPath); // Ensure .claude directory exists await fs.mkdir(claudeDir, { recursive: true }); // Backup current settings before modifying await createBackup("claude", settingsPath); // Read current settings let currentSettings: Record = {}; try { const content = await fs.readFile(settingsPath, "utf-8"); currentSettings = JSON.parse(content); } catch (error: any) { if (error.code !== "ENOENT") { throw error; } } // Claude Code gateway mode expects the unified root endpoint, not a forced /v1 suffix. if (env.ANTHROPIC_BASE_URL) { env.ANTHROPIC_BASE_URL = normalizeClaudeBaseUrl(env.ANTHROPIC_BASE_URL); } // Merge new env with existing settings const newSettings = { ...currentSettings, env: { ...(currentSettings.env || {}), ...env, }, }; // Write new settings await fs.writeFile(settingsPath, JSON.stringify(newSettings, null, 2)); // Persist last-configured timestamp try { saveCliToolLastConfigured("claude"); } catch { /* non-critical */ } return NextResponse.json({ success: true, message: "Settings updated successfully", }); } catch (error) { console.log("Error updating claude settings:", error); return NextResponse.json({ error: "Failed to update claude settings" }, { status: 500 }); } } // Fields to remove when resetting const RESET_ENV_KEYS = [ "ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "API_TIMEOUT_MS", ]; // DELETE - Reset settings (remove env fields) export async function DELETE(request: Request) { const authError = await requireCliToolsAuth(request); if (authError) return authError; try { const writeGuard = ensureCliConfigWriteAllowed(); if (writeGuard) { return NextResponse.json({ error: writeGuard }, { status: 403 }); } const settingsPath = getClaudeSettingsPath(); // Read current settings let currentSettings: Record = {}; try { const content = await fs.readFile(settingsPath, "utf-8"); currentSettings = JSON.parse(content); } catch (error: any) { if (error.code === "ENOENT") { return NextResponse.json({ success: true, message: "No settings file to reset", }); } throw error; } // Backup current settings before resetting await createBackup("claude", settingsPath); // Remove specified env fields if (currentSettings.env) { RESET_ENV_KEYS.forEach((key) => { delete currentSettings.env[key]; }); // Clean up empty env object if (Object.keys(currentSettings.env).length === 0) { delete currentSettings.env; } } // Write updated settings await fs.writeFile(settingsPath, JSON.stringify(currentSettings, null, 2)); // Clear last-configured timestamp try { deleteCliToolLastConfigured("claude"); } catch { /* non-critical */ } return NextResponse.json({ success: true, message: "Settings reset successfully", }); } catch (error) { console.log("Error resetting claude settings:", error); return NextResponse.json({ error: "Failed to reset claude settings" }, { status: 500 }); } }