"use server"; import { NextResponse } from "next/server"; import { exec } from "child_process"; import { promisify } from "util"; import fs from "fs/promises"; import path from "path"; import os from "os"; import { DEFAULT_PLUGINS } from "@/shared/constants/coworkPlugins"; const execAsync = promisify(exec); // Exa MCP def — reuse from coworkPlugins (DRY). const EXA_PLUGIN = DEFAULT_PLUGINS.find((p) => p.name === "exa"); const buildExaMcpEntry = () => ({ type: EXA_PLUGIN.transport, url: EXA_PLUGIN.url, }); // Get claude settings path based on OS const getClaudeSettingsPath = () => { const homeDir = os.homedir(); return path.join(homeDir, ".claude", "settings.json"); }; // Claude Code CLI reads mcpServers from ~/.claude.json (NOT settings.json). const getClaudeJsonPath = () => path.join(os.homedir(), ".claude.json"); const readClaudeJson = async () => { try { const content = await fs.readFile(getClaudeJsonPath(), "utf-8"); return JSON.parse(content.replace(/,(\s*[}\]])/g, "$1")); } catch { return null; } }; const writeClaudeJsonMcp = async (mcpServers) => { const filePath = getClaudeJsonPath(); let data = {}; try { data = JSON.parse(await fs.readFile(filePath, "utf-8")); } catch (error) { if (error.code !== "ENOENT") throw error; } if (mcpServers && Object.keys(mcpServers).length > 0) { data.mcpServers = { ...(data.mcpServers || {}), ...mcpServers }; } else if (data.mcpServers) { delete data.mcpServers.exa; if (Object.keys(data.mcpServers).length === 0) delete data.mcpServers; } await fs.writeFile(filePath, JSON.stringify(data, null, 2)); }; // Check if claude CLI is installed (via which/where or config file exists) const checkClaudeInstalled = async () => { try { const isWindows = os.platform() === "win32"; const command = isWindows ? "where claude" : "which claude"; const env = isWindows ? { ...process.env, PATH: `${process.env.APPDATA}\\npm;${process.env.PATH}` } : process.env; await execAsync(command, { windowsHide: true, env }); return true; } catch { try { await fs.access(getClaudeSettingsPath()); return true; } catch { return false; } } }; // Read current settings const readSettings = async () => { try { const settingsPath = getClaudeSettingsPath(); const content = await fs.readFile(settingsPath, "utf-8"); // Tolerate JSONC (trailing commas) and treat unparseable files as "no config" // rather than throwing a 500 that the UI misreads as "tool not installed". const stripped = content.replace(/,(\s*[}\]])/g, "$1"); return JSON.parse(stripped); } catch (error) { return null; } }; // GET - Check claude CLI and read current settings export async function GET() { try { const isInstalled = await checkClaudeInstalled(); if (!isInstalled) { return NextResponse.json({ installed: false, settings: null, message: "Claude CLI is not installed", }); } const settings = await readSettings(); const has9Router = !!(settings?.env?.ANTHROPIC_BASE_URL); const claudeJson = await readClaudeJson(); return NextResponse.json({ installed: true, settings: settings, has9Router: has9Router, exaMcpEnabled: !!claudeJson?.mcpServers?.exa, 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) { try { const { env, exaMcpEnabled } = await request.json(); if (!env || typeof env !== "object") { return NextResponse.json( { error: "Invalid env object" }, { status: 400 } ); } const settingsPath = getClaudeSettingsPath(); const claudeDir = path.dirname(settingsPath); // Ensure .claude directory exists await fs.mkdir(claudeDir, { recursive: true }); // Read current settings let currentSettings = {}; try { const content = await fs.readFile(settingsPath, "utf-8"); currentSettings = JSON.parse(content); } catch (error) { if (error.code !== "ENOENT") { throw error; } } // Normalize ANTHROPIC_BASE_URL to ensure /v1 suffix if (env.ANTHROPIC_BASE_URL) { env.ANTHROPIC_BASE_URL = env.ANTHROPIC_BASE_URL.endsWith("/v1") ? env.ANTHROPIC_BASE_URL : `${env.ANTHROPIC_BASE_URL}/v1`; } // Merge new env with existing settings const newSettings = { ...currentSettings, hasCompletedOnboarding: true, env: { ...(currentSettings.env || {}), ...env, }, }; // Write new settings await fs.writeFile(settingsPath, JSON.stringify(newSettings, null, 2)); // Exa MCP toggle — write to ~/.claude.json (CLI reads mcpServers from here). if (EXA_PLUGIN) { await writeClaudeJsonMcp(exaMcpEnabled ? { exa: buildExaMcpEntry() } : null); } 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_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "API_TIMEOUT_MS", ]; // DELETE - Reset settings (remove env fields) export async function DELETE() { try { const settingsPath = getClaudeSettingsPath(); // Read current settings let currentSettings = {}; try { const content = await fs.readFile(settingsPath, "utf-8"); currentSettings = JSON.parse(content); } catch (error) { if (error.code === "ENOENT") { return NextResponse.json({ success: true, message: "No settings file to reset", }); } throw error; } // 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; } } // Remove injected MCP servers (Exa) from ~/.claude.json await writeClaudeJsonMcp(null); // Write updated settings await fs.writeFile(settingsPath, JSON.stringify(currentSettings, null, 2)); 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 } ); } }