Skip to main content
Glama

AI Code Toolkit

by AgiFlow
ClaudeCodeService.ts13.4 kB
/** * ClaudeCodeService * * DESIGN PATTERNS: * - Class-based service pattern for encapsulating business logic * - Interface implementation for dependency injection and testing * - Single Responsibility: Manages Claude Code CLI interactions * - Method-based API: Public methods expose service capabilities * * CODING STANDARDS: * - Service class names use PascalCase with 'Service' suffix * - Method names use camelCase with descriptive verbs * - Return types should be explicit (never use implicit any) * - Use async/await for asynchronous operations * - Handle errors with try-catch and throw descriptive Error objects * - Document public methods with JSDoc comments * * AVOID: * - Side effects in constructors (keep them lightweight) * - Mixing concerns (keep services focused on single domain) * - Direct coupling to other services (use dependency injection) * - Exposing internal implementation details */ import { execa } from 'execa'; import * as fs from 'fs-extra'; import * as path from 'node:path'; import * as readline from 'node:readline'; import { v4 as uuidv4 } from 'uuid'; import type { CodingAgentService, LlmInvocationParams, LlmInvocationResponse, McpSettings, PromptConfig, } from '../types'; import { appendUniqueToFile, appendUniqueWithMarkers, writeFileEnsureDir } from '../utils/file'; /** * Internal message types for parsing stream-json output from Claude Code CLI */ interface ClaudeStreamMessage { type: 'system' | 'assistant' | 'user' | 'result'; message?: { id?: string; model?: string; content?: Array<{ type: 'text' | 'tool_use'; text?: string; }>; stop_reason?: string | null; usage?: { input_tokens: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; output_tokens: number; }; }; model?: string; session_id?: string; duration_ms?: number; total_cost_usd?: number; usage?: any; } /** * Claude Code built-in tools that should be disallowed for LLM-only mode */ const CLAUDE_CODE_BUILTIN_TOOLS = [ 'Task', 'Bash', 'Glob', 'Grep', 'LS', 'exit_plan_mode', 'Read', 'Edit', 'MultiEdit', 'Write', 'NotebookRead', 'NotebookEdit', 'WebFetch', 'TodoRead', 'TodoWrite', 'WebSearch', ].join(','); /** * Service for interacting with Claude Code CLI as a coding agent * Provides standard LLM interface using Claude Code's stream-json output format */ export class ClaudeCodeService implements CodingAgentService { private mcpSettings: McpSettings = {}; private promptConfig: PromptConfig = {}; private readonly workspaceRoot: string; private readonly claudePath: string; private readonly defaultTimeout: number; private readonly defaultModel: string; private readonly defaultEnv: Record<string, string>; constructor(options?: { workspaceRoot?: string; claudePath?: string; defaultTimeout?: number; defaultModel?: string; defaultEnv?: Record<string, string>; }) { this.workspaceRoot = options?.workspaceRoot || process.cwd(); this.claudePath = options?.claudePath || 'claude'; this.defaultTimeout = options?.defaultTimeout || 60000; // 1 minute default this.defaultModel = options?.defaultModel || 'claude-sonnet-4-20250514'; this.defaultEnv = options?.defaultEnv || { DISABLE_TELEMETRY: '1', DISABLE_AUTOUPDATER: '1', IS_SANDBOX: '1', CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', }; } /** * Check if the Claude Code service is enabled * Detects Claude Code by checking for .claude folder or CLAUDE.md file in workspace root */ async isEnabled(): Promise<boolean> { const claudeFolder = path.join(this.workspaceRoot, '.claude'); const claudeMdFile = path.join(this.workspaceRoot, 'CLAUDE.md'); const hasClaude = await fs.pathExists(claudeFolder); const hasClaudeMd = await fs.pathExists(claudeMdFile); return hasClaude || hasClaudeMd; } /** * Update MCP (Model Context Protocol) settings for Claude Code * Writes MCP server configuration to .mcp.json in workspace root * Converts standardized McpServerConfig to Claude Code format */ async updateMcpSettings(settings: McpSettings): Promise<void> { this.mcpSettings = { ...this.mcpSettings, ...settings }; // Claude Code uses .mcp.json in workspace root const configPath = path.join(this.workspaceRoot, '.mcp.json'); // Read or create config let config: any = {}; if (await fs.pathExists(configPath)) { const content = await fs.readFile(configPath, 'utf-8'); config = JSON.parse(content); } // Ensure mcpServers key exists if (!config.mcpServers) { config.mcpServers = {}; } // Convert standardized MCP server configs to Claude Code format if (settings.servers) { for (const [serverName, serverConfig] of Object.entries(settings.servers)) { const claudeConfig: any = { type: serverConfig.type, disabled: serverConfig.disabled ?? false, }; // Add type-specific fields if (serverConfig.type === 'stdio') { claudeConfig.command = serverConfig.command; claudeConfig.args = serverConfig.args; if (serverConfig.env) { claudeConfig.env = serverConfig.env; } } else if (serverConfig.type === 'http' || serverConfig.type === 'sse') { claudeConfig.url = serverConfig.url; } config.mcpServers[serverName] = claudeConfig; } } // Write config back with pretty formatting await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`); } /** * Update prompt configuration * * If customInstructionFile is provided, writes the prompt to that file and references it * using @file syntax in CLAUDE.md and AGENTS.md. * * If marker is true, wraps the content with AICODE tracking markers * (<!-- AICODE:START --> and <!-- AICODE:END -->). * * Otherwise, appends the prompt directly to CLAUDE.md and AGENTS.md. */ async updatePrompt(config: PromptConfig): Promise<void> { this.promptConfig = { ...this.promptConfig, ...config }; if (!config.systemPrompt) { return; } const claudeMdPath = path.join(this.workspaceRoot, 'CLAUDE.md'); const agentsMdPath = path.join(this.workspaceRoot, 'AGENTS.md'); if (config.customInstructionFile) { // Write prompt to custom instruction file const customFilePath = path.join(this.workspaceRoot, config.customInstructionFile); await writeFileEnsureDir(customFilePath, config.systemPrompt); // Reference the file in CLAUDE.md and AGENTS.md using @ syntax (without curly braces) const reference = `@${config.customInstructionFile}`; if (config.marker) { // Use AICODE markers to track the reference await appendUniqueWithMarkers( claudeMdPath, reference, reference, `# Claude Code Instructions\n\n<!-- AICODE:START -->\n${reference}\n<!-- AICODE:END -->\n`, ); // Append reference to AGENTS.md if it exists await appendUniqueWithMarkers(agentsMdPath, reference, reference); } else { // Append reference without markers const referenceContent = `\n\n${reference}\n`; await appendUniqueToFile( claudeMdPath, referenceContent, reference, `# Claude Code Instructions\n${referenceContent}`, ); await appendUniqueToFile(agentsMdPath, referenceContent, reference); } } else { // Append prompt directly to CLAUDE.md and AGENTS.md if (config.marker) { // Use AICODE markers to track the prompt content await appendUniqueWithMarkers( claudeMdPath, config.systemPrompt, config.systemPrompt, `# Claude Code Instructions\n\n<!-- AICODE:START -->\n${config.systemPrompt}\n<!-- AICODE:END -->\n`, ); // Append to AGENTS.md if it exists await appendUniqueWithMarkers(agentsMdPath, config.systemPrompt, config.systemPrompt); } else { // Append prompt without markers const promptContent = `\n\n${config.systemPrompt}\n`; await appendUniqueToFile( claudeMdPath, promptContent, config.systemPrompt, `# Claude Code Instructions\n${promptContent}`, ); await appendUniqueToFile(agentsMdPath, promptContent, config.systemPrompt); } } } /** * Invoke Claude Code as an LLM * Executes Claude Code CLI with stream-json output format */ async invokeAsLlm(params: LlmInvocationParams): Promise<LlmInvocationResponse> { const sessionId = uuidv4(); // Build command arguments for single-turn LLM invocation const args = [ '--max-turns', '0', '--output-format', 'stream-json', '--verbose', '--session-id', sessionId, '--disallowedTools', CLAUDE_CODE_BUILTIN_TOOLS, ]; if (params.model) { args.push('--model', params.model); } // Apply system prompt from params (priority) or promptConfig (fallback) const systemPrompt = params.systemPrompt ?? this.promptConfig.systemPrompt; if (systemPrompt) { args.push('--system-prompt', systemPrompt); } // Execute Claude CLI const child = execa(this.claudePath, args, { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', timeout: params.maxTokens ? params.maxTokens * 100 : this.defaultTimeout, maxBuffer: 1024 * 1024 * 100, // 100MB buffer env: { ...process.env, ...this.defaultEnv, ...(params.maxTokens && { CLAUDE_CODE_MAX_OUTPUT_TOKENS: params.maxTokens.toString(), }), }, }); // Write prompt to stdin and close it child.stdin?.write(params.prompt); child.stdin?.end(); // Create readline interface for streaming output const rl = readline.createInterface({ input: child.stdout, }); // Collect response data let responseContent = ''; let model = params.model || this.defaultModel; const usage = { inputTokens: 0, outputTokens: 0, }; let partialData = ''; try { // Process streaming JSON output for await (const line of rl) { if (!line.trim()) continue; let message: ClaudeStreamMessage; try { message = JSON.parse(line); } catch { // Handle partial JSON by accumulating partialData += line; try { message = JSON.parse(partialData); partialData = ''; } catch { continue; } } // Process different message types if (message.type === 'system' && message.model) { model = message.model; } else if (message.type === 'assistant' && message.message) { // Extract text content from assistant messages const textContent = message.message.content ?.filter((c) => c.type === 'text') .map((c) => c.text) .filter(Boolean) .join('') || ''; responseContent += textContent; // Update usage stats if (message.message.usage) { usage.inputTokens = message.message.usage.input_tokens || 0; usage.outputTokens = message.message.usage.output_tokens || 0; } } else if (message.type === 'result') { // Final result with usage info if (message.usage) { usage.inputTokens = message.usage.input_tokens || usage.inputTokens; usage.outputTokens = message.usage.output_tokens || usage.outputTokens; } } } // Wait for process to complete const { exitCode } = await child; if (exitCode !== 0) { throw new Error(`Claude Code process exited with code ${exitCode}`); } // Return standard LLM response return { content: responseContent.trim(), model, usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, }, }; } catch (error) { // Clean up on error rl.close(); if (!child.killed) { child.kill(); } // Provide descriptive error messages based on error type if (error instanceof Error) { if (error.message.includes('ETIMEDOUT') || error.message.includes('timed out')) { throw new Error( `Claude Code invocation timed out after ${params.maxTokens ? params.maxTokens * 100 : this.defaultTimeout}ms. Consider increasing the timeout or reducing maxTokens.`, ); } if (error.message.includes('ENOENT')) { throw new Error( `Claude Code CLI not found at path: ${this.claudePath}. Ensure Claude Code is installed and the path is correct.`, ); } if (error.message.includes('exited with code')) { throw new Error( `Claude Code process failed: ${error.message}. Check Claude Code logs for details.`, ); } throw new Error(`Failed to invoke Claude Code: ${error.message}`); } throw new Error(`Failed to invoke Claude Code: ${String(error)}`); } finally { rl.close(); } } }

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/AgiFlow/aicode-toolkit'

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