Spaces:
Runtime error
Runtime error
| /** | |
| * ACP (Agent Client Protocol) — CLI Agent Registry | |
| * | |
| * Discovers installed CLI tools on the system by checking standard paths | |
| * and running version commands. Used to offer ACP transport as an alternative | |
| * to the HTTP proxy method. | |
| * | |
| * Supports 14 built-in agents + user-defined custom agents from settings. | |
| * | |
| * Reference: https://github.com/iOfficeAI/AionUi (auto-detects CLI agents) | |
| */ | |
| import { execFileSync } from "child_process"; | |
| import path from "path"; | |
| export interface CliAgentInfo { | |
| /** Agent identifier (e.g., "codex", "claude", "goose") */ | |
| id: string; | |
| /** Display name */ | |
| name: string; | |
| /** Binary name to spawn */ | |
| binary: string; | |
| /** Version detection command */ | |
| versionCommand: string; | |
| /** Detected version (null if not installed) */ | |
| version: string | null; | |
| /** Whether the agent is installed and available */ | |
| installed: boolean; | |
| /** Provider ID that this agent maps to in OmniRoute */ | |
| providerAlias: string; | |
| /** Arguments to pass when spawning for ACP */ | |
| spawnArgs: string[]; | |
| /** Protocol used for communication */ | |
| protocol: "stdio" | "http"; | |
| /** Whether this is a user-defined custom agent */ | |
| isCustom?: boolean; | |
| } | |
| /** Shape stored in settings DB for custom agents */ | |
| export interface CustomAgentDef { | |
| id: string; | |
| name: string; | |
| binary: string; | |
| versionCommand: string; | |
| providerAlias: string; | |
| spawnArgs: string[]; | |
| protocol: "stdio" | "http"; | |
| } | |
| /** | |
| * Registry of known CLI agents that support ACP or similar protocols. | |
| */ | |
| const AGENT_DEFINITIONS: Omit<CliAgentInfo, "version" | "installed">[] = [ | |
| { | |
| id: "codex", | |
| name: "OpenAI Codex CLI", | |
| binary: "codex", | |
| versionCommand: "codex --version", | |
| providerAlias: "codex", | |
| spawnArgs: ["--quiet"], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "claude", | |
| name: "Claude Code CLI", | |
| binary: "claude", | |
| versionCommand: "claude --version", | |
| providerAlias: "claude", | |
| spawnArgs: ["--print", "--output-format", "json"], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "goose", | |
| name: "Goose CLI", | |
| binary: "goose", | |
| versionCommand: "goose --version", | |
| providerAlias: "goose", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "gemini-cli", | |
| name: "Gemini CLI", | |
| binary: "gemini", | |
| versionCommand: "gemini --version", | |
| providerAlias: "gemini-cli", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "openclaw", | |
| name: "OpenClaw", | |
| binary: "openclaw", | |
| versionCommand: "openclaw --version", | |
| providerAlias: "openclaw", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "aider", | |
| name: "Aider", | |
| binary: "aider", | |
| versionCommand: "aider --version", | |
| providerAlias: "aider", | |
| spawnArgs: ["--no-auto-commits"], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "opencode", | |
| name: "OpenCode", | |
| binary: "opencode", | |
| versionCommand: "opencode --version", | |
| providerAlias: "opencode", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "cline", | |
| name: "Cline", | |
| binary: "cline", | |
| versionCommand: "cline --version", | |
| providerAlias: "cline", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "qwen-code", | |
| name: "Qwen Code", | |
| binary: "qwen", | |
| versionCommand: "qwen --version", | |
| providerAlias: "qwen", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "forge", | |
| name: "ForgeCode", | |
| binary: "forge", | |
| versionCommand: "forge --version", | |
| providerAlias: "forge", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "amazon-q", | |
| name: "Amazon Q Developer", | |
| binary: "q", | |
| versionCommand: "q --version", | |
| providerAlias: "amazon-q", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "interpreter", | |
| name: "Open Interpreter", | |
| binary: "interpreter", | |
| versionCommand: "interpreter --version", | |
| providerAlias: "interpreter", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "cursor-cli", | |
| name: "Cursor CLI", | |
| binary: "cursor", | |
| versionCommand: "cursor --version", | |
| providerAlias: "cursor", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| { | |
| id: "warp", | |
| name: "Warp AI", | |
| binary: "warp", | |
| versionCommand: "warp --version", | |
| providerAlias: "warp", | |
| spawnArgs: [], | |
| protocol: "stdio", | |
| }, | |
| ]; | |
| // --------------------------------------------------------------------------- | |
| // Detection cache (60 seconds) | |
| // --------------------------------------------------------------------------- | |
| let _cachedAgents: CliAgentInfo[] | null = null; | |
| let _cacheTimestamp = 0; | |
| const CACHE_TTL_MS = 60_000; | |
| /** Custom agents loaded from settings */ | |
| let _customAgentDefs: CustomAgentDef[] = []; | |
| const DISALLOWED_VERSION_COMMAND_CHARS = /[;&|<>`$\r\n]/; | |
| /** | |
| * Set custom agent definitions from settings. | |
| */ | |
| export function setCustomAgents(agents: CustomAgentDef[]): void { | |
| _customAgentDefs = agents || []; | |
| _cachedAgents = null; // invalidate cache | |
| } | |
| /** | |
| * Get current custom agent definitions. | |
| */ | |
| export function getCustomAgentDefs(): CustomAgentDef[] { | |
| return _customAgentDefs; | |
| } | |
| function tokenizeVersionCommand(command: string): string[] | null { | |
| if (!command || DISALLOWED_VERSION_COMMAND_CHARS.test(command)) { | |
| return null; | |
| } | |
| const tokens: string[] = []; | |
| let current = ""; | |
| let quote: '"' | "'" | null = null; | |
| for (let index = 0; index < command.length; index += 1) { | |
| const char = command[index]; | |
| if (quote) { | |
| if (char === quote) { | |
| quote = null; | |
| } else { | |
| current += char; | |
| } | |
| continue; | |
| } | |
| if (char === '"' || char === "'") { | |
| quote = char; | |
| continue; | |
| } | |
| if (/\s/.test(char)) { | |
| if (current) { | |
| tokens.push(current); | |
| current = ""; | |
| } | |
| continue; | |
| } | |
| if (char === "\\") { | |
| const next = command[index + 1]; | |
| if (next) { | |
| current += next; | |
| index += 1; | |
| continue; | |
| } | |
| } | |
| current += char; | |
| } | |
| if (quote) { | |
| return null; | |
| } | |
| if (current) { | |
| tokens.push(current); | |
| } | |
| return tokens.length > 0 ? tokens : null; | |
| } | |
| function normalizeCommandToken(command: string): string { | |
| return path.normalize(command).replace(/\\/g, "/").toLowerCase(); | |
| } | |
| export function resolveVersionProbe( | |
| binary: string, | |
| versionCommand: string, | |
| requireBinaryMatch = false | |
| ): { command: string; args: string[] } | null { | |
| const tokens = tokenizeVersionCommand(versionCommand); | |
| if (!tokens) { | |
| return null; | |
| } | |
| const [command, ...args] = tokens; | |
| if (!command) { | |
| return null; | |
| } | |
| if (requireBinaryMatch) { | |
| const normalizedCommand = normalizeCommandToken(command); | |
| const allowed = new Set([ | |
| normalizeCommandToken(binary), | |
| normalizeCommandToken(path.basename(binary)), | |
| ]); | |
| if (!allowed.has(normalizedCommand)) { | |
| return null; | |
| } | |
| } | |
| return { command, args }; | |
| } | |
| export function shouldUseShellForVersionProbe( | |
| command: string, | |
| platform = process.platform | |
| ): boolean { | |
| if (platform !== "win32") return false; | |
| const normalized = command.trim().toLowerCase(); | |
| if (!normalized) return false; | |
| return ( | |
| normalized.endsWith(".cmd") || normalized.endsWith(".bat") || path.extname(normalized) === "" | |
| ); | |
| } | |
| /** | |
| * Detect a single agent by running its version command. | |
| */ | |
| function detectAgent( | |
| def: Omit<CliAgentInfo, "version" | "installed">, | |
| isCustom = false | |
| ): CliAgentInfo { | |
| let version: string | null = null; | |
| let installed = false; | |
| try { | |
| const probe = resolveVersionProbe(def.binary, def.versionCommand, isCustom); | |
| if (!probe) { | |
| return { ...def, version, installed, isCustom }; | |
| } | |
| const output = execFileSync(probe.command, probe.args, { | |
| timeout: 5000, | |
| encoding: "utf-8", | |
| stdio: ["pipe", "pipe", "pipe"], | |
| ...(shouldUseShellForVersionProbe(probe.command) ? { shell: true } : {}), | |
| }).trim(); | |
| // Extract version number from output | |
| const versionMatch = output.match(/(\d+\.\d+\.\d+(?:-\w+)?)/); | |
| version = versionMatch ? versionMatch[1] : output.split("\n")[0]; | |
| installed = true; | |
| } catch { | |
| // Not installed or not runnable | |
| } | |
| return { ...def, version, installed, isCustom }; | |
| } | |
| /** | |
| * Detect installed CLI agents on the system. | |
| * Results are cached for 60 seconds. | |
| */ | |
| export function detectInstalledAgents(): CliAgentInfo[] { | |
| const now = Date.now(); | |
| if (_cachedAgents && now - _cacheTimestamp < CACHE_TTL_MS) { | |
| return _cachedAgents; | |
| } | |
| // Merge built-in + custom definitions | |
| const allDefs = [ | |
| ...AGENT_DEFINITIONS.map((d) => ({ ...d, _custom: false })), | |
| ..._customAgentDefs.map((d) => ({ ...d, _custom: true })), | |
| ]; | |
| _cachedAgents = allDefs.map((def) => { | |
| const { _custom, ...rest } = def; | |
| return detectAgent(rest, _custom); | |
| }); | |
| _cacheTimestamp = now; | |
| return _cachedAgents; | |
| } | |
| /** | |
| * Force refresh detection cache. | |
| */ | |
| export function refreshAgentCache(): CliAgentInfo[] { | |
| _cachedAgents = null; | |
| return detectInstalledAgents(); | |
| } | |
| /** | |
| * Get a specific agent by ID. | |
| */ | |
| export function getAgentById(id: string): CliAgentInfo | undefined { | |
| const agents = detectInstalledAgents(); | |
| return agents.find((a) => a.id === id); | |
| } | |
| /** | |
| * Get agents that are installed and available for ACP. | |
| */ | |
| export function getAvailableAgents(): CliAgentInfo[] { | |
| return detectInstalledAgents().filter((a) => a.installed); | |
| } | |