"use server"; import { NextResponse } from "next/server"; import fs from "fs/promises"; import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; import { getCliRuntimeStatus, CLI_TOOL_IDS, getCliPrimaryConfigPath, } from "@/shared/services/cliRuntime"; import { getAllCliToolLastConfigured } from "@/lib/db/cliToolState"; import { getRuntimePorts } from "@/lib/runtime/ports"; const { apiPort } = getRuntimePorts(); // Check if a tool has OmniRoute configured by reading its config file directly // This replaces the expensive self-referential HTTP calls to /api/cli-tools/*-settings async function checkToolConfigStatus(toolId: string): Promise { try { const configPath = getCliPrimaryConfigPath(toolId); if (!configPath) return "unknown"; const content = await fs.readFile(configPath, "utf-8"); // Codex uses TOML config — parse as raw text, not JSON if (toolId === "codex") { const lower = content.toLowerCase(); const hasOmniRoute = lower.includes("omniroute") || lower.includes(`localhost:${apiPort}`) || lower.includes(`127.0.0.1:${apiPort}`); if (!hasOmniRoute) return "not_configured"; // Also verify auth.json has an API key (not masked/empty) try { const authPath = configPath.replace(/config\.toml$/, "auth.json"); const authContent = await fs.readFile(authPath, "utf-8"); const auth = JSON.parse(authContent); const apiKey = auth?.OPENAI_API_KEY || ""; if (!apiKey || apiKey.includes("****") || apiKey.length < 20) { return "not_configured"; } } catch { return "not_configured"; } return "configured"; } const config = JSON.parse(content); // Each tool stores OmniRoute config differently switch (toolId) { case "claude": return config?.env?.ANTHROPIC_BASE_URL ? "configured" : "not_configured"; case "qwen": // Check modelProviders for OmniRoute entries const mp = config?.modelProviders; if (!mp) return "not_configured"; const qwenConfigStr = JSON.stringify(mp).toLowerCase(); return qwenConfigStr.includes("omniroute") || qwenConfigStr.includes(`localhost:${apiPort}`) || qwenConfigStr.includes(`127.0.0.1:${apiPort}`) ? "configured" : "not_configured"; case "droid": case "openclaw": case "cline": case "kilo": // Generic check: look for OmniRoute-specific markers in the config const configStr = JSON.stringify(config).toLowerCase(); if ( configStr.includes("omniroute") || configStr.includes("sk_omniroute") || configStr.includes(`localhost:${apiPort}`) || configStr.includes(`127.0.0.1:${apiPort}`) ) { return "configured"; } // Also accept openai-compatible provider with any non-empty baseUrl // (user may configure an external domain instead of localhost) if ( toolId === "cline" && (config.actModeApiProvider === "openai" || config.planModeApiProvider === "openai") && (config.openAiBaseUrl || "").trim().length > 0 ) { return "configured"; } return "not_configured"; default: return "unknown"; } } catch { return "not_configured"; } } /** * GET /api/cli-tools/status * Returns runtime + config status for all CLI tools in one batch call. * Used by the CLI Tools page to show status badges in collapsed state. */ export async function GET(request: Request) { const authError = await requireCliToolsAuth(request); if (authError) return authError; try { const statuses = {}; // Run all runtime checks in parallel with individual timeouts const RUNTIME_CHECK_TIMEOUT = 5000; // 5s per tool max await Promise.all( CLI_TOOL_IDS.map(async (toolId) => { try { const runtime = (await Promise.race([ getCliRuntimeStatus(toolId), new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), RUNTIME_CHECK_TIMEOUT) ), ])) as { installed: boolean; runnable: boolean; command?: string; commandPath?: string; reason?: string; }; statuses[toolId] = { installed: runtime.installed, runnable: runtime.runnable, command: runtime.command, commandPath: runtime.commandPath, reason: runtime.reason || null, }; } catch (error) { statuses[toolId] = { installed: false, runnable: false, reason: error.message || "Check failed", }; } }) ); // Check config status for installed+runnable tools via direct file reads const settingsTools = ["claude", "codex", "droid", "openclaw", "cline", "kilo", "qwen"]; await Promise.all( settingsTools.map(async (toolId) => { if (!statuses[toolId]) { return; } if (!statuses[toolId].installed || !statuses[toolId].runnable) { statuses[toolId].configStatus = "not_installed"; return; } statuses[toolId].configStatus = await checkToolConfigStatus(toolId); }) ); // Merge last-configured timestamps from SQLite try { const lastConfigured = getAllCliToolLastConfigured(); for (const [toolId, timestamp] of Object.entries(lastConfigured)) { if (statuses[toolId]) { statuses[toolId].lastConfiguredAt = timestamp; } } } catch { /* non-critical */ } return NextResponse.json(statuses); } catch (error) { console.log("Error fetching CLI tool statuses:", error); return NextResponse.json({ error: "Failed to fetch statuses" }, { status: 500 }); } }