Skip to main content
Glama
command-builder.tsโ€ข7.98 kB
/** * Claude Command Builder - Single source of truth for building Claude CLI commands * * Extracts command building logic from transport layer for better testability and DRY. * This module is transport-agnostic - it builds the command, transports execute it. */ import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import type { IrisConfig } from "../process-pool/types.js"; /** * Command information for transports to execute */ export interface CommandInfo { /** Claude executable path (e.g., 'claude', '~/.local/bin/claude') */ executable: string; /** CLI arguments to pass to Claude */ args: string[]; /** Working directory (project path) */ cwd: string; } /** * MCP server configuration structure */ export interface McpConfig { mcpServers: { [serverName: string]: { type: string; url: string; }; }; } /** * Load and render team identity prompt template */ function loadTeamIdentityPrompt(teamName: string): string { try { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const templatePath = join( __dirname, "../templates", "team-identity-prompt.txt", ); const template = readFileSync(templatePath, "utf8"); // Simple template rendering: replace {{teamName}} with actual team name return template.replace(/\{\{teamName\}\}/g, teamName); } catch (error) { // Fallback if template file not found return `# Iris MCP ${teamName}\n\nThis is the **${teamName}** team configured in the Iris MCP server for cross-project Claude coordination.`; } } const irisTools = [ "mcp__iris__team_tell", "mcp__iris__team_quick_tell", "mcp__iris__team_cancel", "mcp__iris__team_reboot", "mcp__iris__team_delete", "mcp__iris__team_compact", "mcp__iris__team_fork", "mcp__iris__team_isAwake", "mcp__iris__team_wake", "mcp__iris__team_sleep", "mcp__iris__team_wake_all", "mcp__iris__team_report", "mcp__iris__team_teams", "mcp__iris__team_debug", "mcp__iris__permissions__approve", "mcp__iris__team_date", ]; /** * ClaudeCommandBuilder - Builds Claude CLI commands based on configuration * * This is a pure function module - no state, no side effects, 100% testable. */ export class ClaudeCommandBuilder { /** * Build complete Claude command information * * @param teamName - Team name (for identity prompt) * @param irisConfig - Team configuration * @param sessionId - Session ID for --resume * @param interactive - If true, build for interactive mode (default: false for headless) * @param fork - If true, append --fork-session flag (default: false) * @returns CommandInfo with executable, args, and cwd */ static build( teamName: string, irisConfig: IrisConfig, sessionId: string, interactive = false, fork = false, ): CommandInfo { const args: string[] = []; // 1. Session management // Resume existing session (unless in test mode) const isTestMode = process.env.NODE_ENV === "test" || process.env.IRIS_TEST_REMOTE === "1"; if (!isTestMode) { args.push("--resume", sessionId); } // 2. Debug mode (always enabled for all sessions) args.push("--debug"); // 3. Headless mode with stream-json I/O (only for non-interactive) if (!interactive) { args.push( "--print", // Non-interactive headless mode "--verbose", // Required for stream-json output "--input-format", "stream-json", "--output-format", "stream-json", ); } // 5. Build tool allowlist based on grantPermission setting const allowedTools = new Set<string>(); const grantPermission = irisConfig.grantPermission || "yes"; // Default to "yes" for backwards compatibility // Use session-specific server name to match the MCP config // This ensures permissions__approve gets session context via the session-specific MCP connection const permissionTool = `mcp__iris-${sessionId}__permissions__approve`; if (grantPermission === "yes") { // Auto-approve all Iris MCP tools irisTools.forEach((tool) => allowedTools.add(tool)); // Add user-specified tools from config if (irisConfig.allowedTools) { const userTools = irisConfig.allowedTools .split(/[,\s]+/) .filter((t) => t); userTools.forEach((tool) => allowedTools.add(tool)); } // Still use permission-prompt-tool for tools not in allowed-tools args.push("--permission-prompt-tool", permissionTool); } else if (grantPermission === "ask") { // Only add permission approval tool, require approval for everything allowedTools.add(permissionTool); args.push("--permission-prompt-tool", permissionTool); } // else grantPermission === "no": no tools allowed, no permission-prompt-tool (block everything) // Add allowedTools if we have any // TODO: Temporarily disabled - may be blocking MCP tools // if (allowedTools.size > 0) { // args.push("--allowed-tools", Array.from(allowedTools).join(",")); // } if (irisConfig.disallowedTools) { args.push("--disallowed-tools", irisConfig.disallowedTools); } // 7. Fork session flag (for interactive terminal forks) if (fork) { args.push("--fork-session"); } // 8. System prompt (team identity + custom append) // Currently commented out - uncomment when ready to enable // const teamIdentity = loadTeamIdentityPrompt(teamName); // const systemPrompt = irisConfig.appendSystemPrompt // ? `${teamIdentity}\n\n${irisConfig.appendSystemPrompt}` // : teamIdentity; // args.push("--append-system-prompt", systemPrompt); // 9. MCP configuration will be handled by transports // They will write the config to a file and add --mcp-config <filepath> // Note: --mcp-config expects a file path, NOT stringified JSON // 10. Determine executable const executable = irisConfig.claudePath || "claude"; return { executable, args, cwd: irisConfig.path, }; } /** * Build MCP configuration object * * For local: Uses direct HTTP connection to localhost * For remote: Uses reverse tunnel with configurable protocol * * NOTE: This is now public so transports can use it to generate MCP config files */ static buildMcpConfig(irisConfig: IrisConfig, sessionId: string): McpConfig { // Determine MCP port (from config or env var or default) const mcpPort = irisConfig.enableReverseMcp ? irisConfig.reverseMcpPort || 1615 : parseInt(process.env.IRIS_HTTP_PORT || "1615", 10); // Determine protocol (HTTP for local or dev mode, HTTPS for production remote) const protocol = !irisConfig.remote || irisConfig.allowHttp ? "http" : "https"; // Build session-specific URL const mcpUrl = `${protocol}://localhost:${mcpPort}/mcp/${sessionId}`; // Use session-specific server name so it doesn't conflict with global iris config // This allows permissions__approve to get session context while other tools use global config const serverName = `iris-${sessionId}`; return { mcpServers: { [serverName]: { type: "http", url: mcpUrl, }, }, }; } /** * Get the MCP config file path for a team session * * Returns: <team-path>/<sessionMcpPath>/iris-mcp-<sessionId>.json * * @param teamPath - Absolute path to team's project directory * @param sessionId - Session ID * @param sessionMcpPath - MCP directory path relative to team path (defaults to ".claude/iris/mcp") * @returns Absolute path to MCP config file */ static getMcpConfigPath( teamPath: string, sessionId: string, sessionMcpPath = ".claude/iris/mcp", ): string { return join(teamPath, sessionMcpPath, `iris-mcp-${sessionId}.json`); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jenova-marie/iris-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server