import { promises as fs } from "fs"; import os from "os"; import path from "path"; import express from "express"; import { providerModelsService } from "../modules/providers/services/provider-models.service.js"; import { parseFrontMatter } from "../shared/frontmatter.js"; import { findAppRoot, getModuleDir } from "../utils/runtime-paths.js"; const __dirname = getModuleDir(import.meta.url); // This route reads the top-level package.json for the status command, so it needs the real // app root even after compilation moves the route file under dist-server/server/routes. const APP_ROOT = findAppRoot(__dirname); const router = express.Router(); const MODEL_PROVIDERS = ["claude", "cursor", "codex", "gemini", "opencode"]; const MODEL_PROVIDER_LABELS = { claude: "Claude", cursor: "Cursor", codex: "Codex", gemini: "Gemini", opencode: "OpenCode", }; const readModelProvider = (value) => { if (typeof value !== "string") { return "claude"; } const normalized = value.trim().toLowerCase(); return MODEL_PROVIDERS.includes(normalized) ? normalized : "claude"; }; const getProviderModelOptions = (provider, context) => { if (provider !== "opencode") { return undefined; } const cwd = typeof context?.projectPath === "string" ? context.projectPath : undefined; return { cwd }; }; export const executeModelsCommand = async (args, context) => { const currentProvider = readModelProvider(context?.provider); const catalog = await providerModelsService.getProviderModels( currentProvider, getProviderModelOptions(currentProvider, context), ); const availableModels = catalog.OPTIONS.map((option) => option.value); const availableOptions = catalog.OPTIONS.map((option) => ({ value: option.value, label: option.label, })); const currentModel = typeof context?.model === "string" && context.model ? context.model : catalog.DEFAULT; return { type: "builtin", action: "models", data: { current: { provider: currentProvider, providerLabel: MODEL_PROVIDER_LABELS[currentProvider], model: currentModel, }, available: { [currentProvider]: availableModels, }, availableModels, availableOptions, defaultModel: catalog.DEFAULT, message: `Current model: ${currentModel}`, }, }; }; /** * Recursively scan directory for command files (.md) * @param {string} dir - Directory to scan * @param {string} baseDir - Base directory for relative paths * @param {string} namespace - Namespace for commands (e.g., 'project', 'user') * @returns {Promise} Array of command objects */ async function scanCommandsDirectory(dir, baseDir, namespace) { const commands = []; try { // Check if directory exists await fs.access(dir); const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { // Recursively scan subdirectories const subCommands = await scanCommandsDirectory( fullPath, baseDir, namespace, ); commands.push(...subCommands); } else if (entry.isFile() && entry.name.endsWith(".md")) { // Parse markdown file for metadata try { const content = await fs.readFile(fullPath, "utf8"); const { data: frontmatter, content: commandContent } = parseFrontMatter(content); // Calculate relative path from baseDir for command name const relativePath = path.relative(baseDir, fullPath); // Remove .md extension and convert to command name const commandName = "/" + relativePath.replace(/\.md$/, "").replace(/\\/g, "/"); // Extract description from frontmatter or first line of content let description = frontmatter.description || ""; if (!description) { const firstLine = commandContent.trim().split("\n")[0]; description = firstLine.replace(/^#+\s*/, "").trim(); } commands.push({ name: commandName, path: fullPath, relativePath, description, namespace, metadata: frontmatter, }); } catch (err) { console.error(`Error parsing command file ${fullPath}:`, err.message); } } } } catch (err) { // Directory doesn't exist or can't be accessed - this is okay if (err.code !== "ENOENT" && err.code !== "EACCES") { console.error(`Error scanning directory ${dir}:`, err.message); } } return commands; } /** * Built-in commands that are always available */ const builtInCommands = [ { name: "/help", description: "Show help documentation for Claude Code", namespace: "builtin", metadata: { type: "builtin" }, }, { name: "/models", description: "View available models for the current provider", namespace: "builtin", metadata: { type: "builtin" }, }, { name: "/cost", description: "Display token usage and cost information", namespace: "builtin", metadata: { type: "builtin" }, }, { name: "/memory", description: "Open CLAUDE.md memory file for editing", namespace: "builtin", metadata: { type: "builtin" }, }, { name: "/config", description: "Open settings and configuration", namespace: "builtin", metadata: { type: "builtin" }, }, { name: "/status", description: "Show system status and version information", namespace: "builtin", metadata: { type: "builtin" }, }, ]; /** * Built-in command handlers * Each handler returns { type: 'builtin', action: string, data: any } */ const builtInHandlers = { "/help": async (args, context) => { const helpText = `# Claude Code Commands ## Built-in Commands ${builtInCommands .map( (cmd) => `### ${cmd.name} ${cmd.description} `, ) .join("\n")} ## Custom Commands Custom commands can be created in: - Project: \`.claude/commands/\` (project-specific) - User: \`~/.claude/commands/\` (available in all projects) ### Command Syntax - **Arguments**: Use \`$ARGUMENTS\` for all args or \`$1\`, \`$2\`, etc. for positional - **File Includes**: Use \`@filename\` to include file contents - **Bash Commands**: Use \`!command\` to execute bash commands ### Examples \`\`\`markdown /mycommand arg1 arg2 \`\`\` `; return { type: "builtin", action: "help", data: { content: helpText, format: "markdown", commands: builtInCommands.map((command) => ({ name: command.name, description: command.description, namespace: command.namespace, })), }, }; }, "/models": executeModelsCommand, "/cost": async (args, context) => { const tokenUsage = context?.tokenUsage || {}; const provider = readModelProvider(context?.provider); const catalog = await providerModelsService.getProviderModels( provider, getProviderModelOptions(provider, context), ); const model = context?.model || catalog.DEFAULT; const used = Number( tokenUsage.used ?? tokenUsage.totalUsed ?? tokenUsage.total_tokens ?? 0, ) || 0; const total = Number( tokenUsage.total ?? tokenUsage.contextWindow ?? parseInt(process.env.CONTEXT_WINDOW || "160000", 10), ) || 160000; const percentage = total > 0 ? Number(((used / total) * 100).toFixed(1)) : 0; const inputTokensRaw = Number( tokenUsage.inputTokens ?? tokenUsage.input ?? tokenUsage.cumulativeInputTokens ?? tokenUsage.promptTokens ?? 0, ) || 0; const outputTokens = Number( tokenUsage.outputTokens ?? tokenUsage.output ?? tokenUsage.cumulativeOutputTokens ?? tokenUsage.completionTokens ?? 0, ) || 0; const cacheTokens = Number( tokenUsage.cacheReadTokens ?? tokenUsage.cacheCreationTokens ?? tokenUsage.cacheTokens ?? tokenUsage.cachedTokens ?? 0, ) || 0; // If we only have total used tokens, treat them as input for display/estimation. const inputTokens = inputTokensRaw > 0 || outputTokens > 0 || cacheTokens > 0 ? inputTokensRaw + cacheTokens : used; // Rough default rates by provider (USD / 1M tokens). const pricingByProvider = { claude: { input: 3, output: 15 }, cursor: { input: 3, output: 15 }, codex: { input: 1.5, output: 6 }, }; const rates = pricingByProvider[provider] || pricingByProvider.claude; const inputCost = (inputTokens / 1_000_000) * rates.input; const outputCost = (outputTokens / 1_000_000) * rates.output; const totalCost = inputCost + outputCost; return { type: "builtin", action: "cost", data: { tokenUsage: { used, total, percentage, }, tokenBreakdown: { input: inputTokens, output: outputTokens, cache: cacheTokens, }, cost: { input: inputCost.toFixed(4), output: outputCost.toFixed(4), total: totalCost.toFixed(4), }, provider, model, }, }; }, "/status": async (args, context) => { // Read version from package.json const packageJsonPath = path.join(APP_ROOT, "package.json"); let version = "unknown"; let packageName = "claude-code-ui"; try { const packageJson = JSON.parse( await fs.readFile(packageJsonPath, "utf8"), ); version = packageJson.version; packageName = packageJson.name; } catch (err) { console.error("Error reading package.json:", err); } const uptime = process.uptime(); const uptimeMinutes = Math.floor(uptime / 60); const uptimeHours = Math.floor(uptimeMinutes / 60); const uptimeFormatted = uptimeHours > 0 ? `${uptimeHours}h ${uptimeMinutes % 60}m` : `${uptimeMinutes}m`; const statusProvider = readModelProvider(context?.provider); const statusCatalog = await providerModelsService.getProviderModels( statusProvider, getProviderModelOptions(statusProvider, context), ); const memoryUsage = process.memoryUsage(); return { type: "builtin", action: "status", data: { version, packageName, uptime: uptimeFormatted, uptimeSeconds: Math.floor(uptime), model: context?.model || statusCatalog.DEFAULT, provider: statusProvider, nodeVersion: process.version, platform: process.platform, pid: process.pid, memoryUsage: { rssMb: Math.round(memoryUsage.rss / 1024 / 1024), heapUsedMb: Math.round(memoryUsage.heapUsed / 1024 / 1024), heapTotalMb: Math.round(memoryUsage.heapTotal / 1024 / 1024), }, }, }; }, "/memory": async (args, context) => { const projectPath = context?.projectPath; if (!projectPath) { return { type: "builtin", action: "memory", data: { error: "No project selected", message: "Please select a project to access its CLAUDE.md file", }, }; } const claudeMdPath = path.join(projectPath, "CLAUDE.md"); // Check if CLAUDE.md exists let exists = false; try { await fs.access(claudeMdPath); exists = true; } catch (err) { // File doesn't exist } return { type: "builtin", action: "memory", data: { path: claudeMdPath, exists, message: exists ? `Opening CLAUDE.md at ${claudeMdPath}` : `CLAUDE.md not found at ${claudeMdPath}. Create it to store project-specific instructions.`, }, }; }, "/config": async (args, context) => { return { type: "builtin", action: "config", data: { message: "Opening settings...", }, }; }, }; /** * POST /api/commands/list * List all available commands from project and user directories */ router.post("/list", async (req, res) => { try { const { projectPath } = req.body; const allCommands = [...builtInCommands]; // Scan project-level commands (.claude/commands/) if (projectPath) { const projectCommandsDir = path.join(projectPath, ".claude", "commands"); const projectCommands = await scanCommandsDirectory( projectCommandsDir, projectCommandsDir, "project", ); allCommands.push(...projectCommands); } // Scan user-level commands (~/.claude/commands/) const homeDir = os.homedir(); const userCommandsDir = path.join(homeDir, ".claude", "commands"); const userCommands = await scanCommandsDirectory( userCommandsDir, userCommandsDir, "user", ); allCommands.push(...userCommands); // Separate built-in and custom commands const customCommands = allCommands.filter( (cmd) => cmd.namespace !== "builtin", ); // Sort commands alphabetically by name customCommands.sort((a, b) => a.name.localeCompare(b.name)); res.json({ builtIn: builtInCommands, custom: customCommands, count: allCommands.length, }); } catch (error) { console.error("Error listing commands:", error); res.status(500).json({ error: "Failed to list commands", message: error.message, }); } }); /** * POST /api/commands/execute * Execute a command with argument replacement * This endpoint prepares the command content but doesn't execute bash commands yet * (that will be handled in the command parser utility) */ router.post("/execute", async (req, res) => { try { const { commandName, commandPath, args = [], context = {} } = req.body; if (!commandName) { return res.status(400).json({ error: "Command name is required", }); } // Handle built-in commands const handler = builtInHandlers[commandName]; if (handler) { try { const result = await handler(args, context); return res.json({ ...result, command: commandName, }); } catch (error) { console.error( `Error executing built-in command ${commandName}:`, error, ); return res.status(500).json({ error: "Command execution failed", message: error.message, command: commandName, }); } } // Handle custom commands if (!commandPath) { return res.status(400).json({ error: "Command path is required for custom commands", }); } // Load command content // Security: validate commandPath is within allowed directories { const resolvedPath = path.resolve(commandPath); const userBase = path.resolve( path.join(os.homedir(), ".claude", "commands"), ); const projectBase = context?.projectPath ? path.resolve(path.join(context.projectPath, ".claude", "commands")) : null; const isUnder = (base) => { const rel = path.relative(base, resolvedPath); return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel); }; if (!(isUnder(userBase) || (projectBase && isUnder(projectBase)))) { return res.status(403).json({ error: "Access denied", message: "Command must be in .claude/commands directory", }); } } const content = await fs.readFile(commandPath, "utf8"); const { data: metadata, content: commandContent } = parseFrontMatter(content); // Basic argument replacement (will be enhanced in command parser utility) let processedContent = commandContent; // Replace $ARGUMENTS with all arguments joined const argsString = args.join(" "); processedContent = processedContent.replace(/\$ARGUMENTS/g, argsString); // Replace $1, $2, etc. with positional arguments args.forEach((arg, index) => { const placeholder = `$${index + 1}`; processedContent = processedContent.replace( new RegExp(`\\${placeholder}\\b`, "g"), arg, ); }); res.json({ type: "custom", command: commandName, content: processedContent, metadata, hasFileIncludes: processedContent.includes("@"), hasBashCommands: processedContent.includes("!"), }); } catch (error) { if (error.code === "ENOENT") { return res.status(404).json({ error: "Command not found", message: `Command file not found: ${req.body.commandPath}`, }); } console.error("Error executing command:", error); res.status(500).json({ error: "Failed to execute command", message: error.message, }); } }); export default router;