| import os from "node:os";
|
| import path from "node:path";
|
| import { execFile } from "node:child_process";
|
| import { promisify } from "node:util";
|
| import { getCurrentHermesAgentRoles } from "./config-generator/hermes-agent";
|
|
|
| const execFileAsync = promisify(execFile);
|
| let execFileImpl = execFileAsync;
|
|
|
| export function __setExecFileImpl(fn: typeof execFileAsync): void {
|
| execFileImpl = fn;
|
| }
|
|
|
| export interface DetectedTool {
|
| id: string;
|
| name: string;
|
| installed: boolean;
|
| version?: string;
|
| configPath: string;
|
| configured: boolean;
|
| configContents?: string;
|
|
|
|
|
| hermesAgentRoles?: Record<
|
| string,
|
| {
|
| model: string;
|
| provider?: string;
|
| usingOmniRoute: boolean;
|
| }
|
| >;
|
| }
|
|
|
| const TOOLS = [
|
| { id: "claude", name: "Claude Code", configPath: "~/.claude/settings.json" },
|
| { id: "codex", name: "Codex CLI", configPath: "~/.codex/config.yaml" },
|
| { id: "opencode", name: "OpenCode", configPath: "~/.config/opencode/opencode.json" },
|
| { id: "cline", name: "Cline", configPath: "~/.cline/data/globalState.json" },
|
| { id: "kilocode", name: "Kilo Code", configPath: "~/.config/kilocode/settings.json" },
|
| { id: "continue", name: "Continue", configPath: "~/.continue/config.yaml" },
|
| { id: "hermes", name: "Hermes", configPath: "~/.hermes/config.yaml" },
|
| { id: "hermes-agent", name: "Hermes Agent", configPath: "~/.hermes/config.yaml" },
|
| { id: "openclaw", name: "OpenClaw", configPath: "~/.openclaw/openclaw.json" },
|
| ] as const;
|
|
|
| const BINARY_NAMES: Record<string, string> = {
|
| claude: "claude",
|
| codex: "codex",
|
| opencode: "opencode",
|
| cline: "cline",
|
| kilocode: "kilocode",
|
| continue: "continue",
|
| hermes: "hermes",
|
| "hermes-agent": "hermes",
|
| openclaw: "openclaw",
|
| };
|
|
|
| function expandHome(p: string): string {
|
| const home = os.homedir();
|
| return p.replace(/^~\//, home + "/");
|
| }
|
|
|
| function isConfigured(content: string, baseUrl: string): boolean {
|
| const normalized = baseUrl.replace(/\/+$/, "");
|
| return (
|
| content.includes(normalized) ||
|
| content.includes("localhost:20128") ||
|
| content.includes("OMNIROUTE_BASE_URL")
|
| );
|
| }
|
|
|
| async function detectBinary(name: string): Promise<{ installed: boolean; version?: string }> {
|
| const binary = BINARY_NAMES[name] || name;
|
| try {
|
| const { stdout } = await execFileImpl(binary, ["--version"], { timeout: 5000 });
|
| const version = stdout.trim().replace(/^v/, "");
|
| return { installed: true, version };
|
| } catch {
|
| try {
|
|
|
| const { stdout } = await execFileAsync("which", [binary], { timeout: 5000 });
|
| if (stdout.trim()) {
|
| return { installed: true };
|
| }
|
| } catch {}
|
| return { installed: false };
|
| }
|
| }
|
|
|
| async function readConfigFile(configPath: string): Promise<string | null> {
|
| try {
|
| const { readFileSync } = await import("node:fs");
|
| const expanded = expandHome(configPath);
|
| if (!expanded) return null;
|
| return readFileSync(expanded, "utf-8");
|
| } catch {
|
| return null;
|
| }
|
| }
|
|
|
| export async function detectTool(id: string): Promise<DetectedTool | null> {
|
| const tool = TOOLS.find((t) => t.id === id);
|
| if (!tool) return null;
|
|
|
| const { installed, version } = await detectBinary(tool.id);
|
| const configPath = expandHome(tool.configPath);
|
| const configContents = await readConfigFile(tool.configPath);
|
| const configured = !!configContents && isConfigured(configContents, "http://localhost:20128");
|
|
|
| const result: DetectedTool = {
|
| id: tool.id,
|
| name: tool.name,
|
| installed,
|
| version,
|
| configPath,
|
| configured,
|
| configContents: configContents ?? undefined,
|
| };
|
|
|
|
|
| if (tool.id === "hermes-agent") {
|
| try {
|
| const roles = await getCurrentHermesAgentRoles();
|
| const richRoles: Record<string, any> = {};
|
|
|
| Object.entries(roles).forEach(([role, info]) => {
|
| const usingOmni =
|
| info?.provider === "omniroute" ||
|
| (info?.base_url || "").includes("20128") ||
|
| (info?.base_url || "").includes("localhost:20128");
|
|
|
| richRoles[role] = {
|
| model: info.model,
|
| provider: info.provider,
|
| usingOmniRoute: usingOmni,
|
| };
|
| });
|
|
|
| result.hermesAgentRoles = richRoles;
|
| } catch {
|
|
|
| }
|
| }
|
|
|
| return result;
|
| }
|
|
|
| export async function detectAllTools(): Promise<DetectedTool[]> {
|
| const results = await Promise.allSettled(TOOLS.map((t) => detectTool(t.id)));
|
|
|
| return results
|
| .filter((r) => r.status === "fulfilled" && r.value !== null)
|
| .map((r) => (r as PromiseFulfilledResult<DetectedTool>).value);
|
| }
|
|
|