fiemwl / src /lib /cli-helper /tool-detector.ts
automindy's picture
Upload 1980 files
6111b2b verified
Raw
History Blame Contribute Delete
4.81 kB
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;
// Rich per-role status for Hermes Agent
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 {
// Try `which` as fallback
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,
};
// Rich per-role status only for Hermes Agent
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 {
// ignore – rich status is optional
}
}
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);
}