Skip to main content
Glama
systempromptio

SystemPrompt Coding Agent

Official
claude-code-service.ts16.8 kB
/** * @fileoverview Modern Claude Code service implementation * @module services/claude-code/claude-code-service * * @remarks * This service provides the main interface for interacting with Claude Code CLI. * It manages Claude sessions, executes queries, and tracks task progress. * The service can operate in two modes: * 1. Direct SDK mode when ANTHROPIC_API_KEY is available * 2. Host proxy mode when using the system Claude installation * * @example * ```typescript * import { ClaudeCodeService } from './services/claude-code'; * * const service = ClaudeCodeService.getInstance(); * * // Create a session * const sessionId = await service.createSession({ * workingDirectory: '/path/to/project' * }); * * // Execute a query * const result = await service.querySync( * sessionId, * 'Implement user authentication' * ); * ``` */ import { EventEmitter } from 'events'; import type { ClaudeCodeSession, ClaudeCodeOptions, ProgressEvent, StreamEvent } from './types.js'; import type { SDKMessage } from '@anthropic-ai/claude-code'; import { ClaudeEvent, ClaudeEventType } from '../../types/claude-events.js'; import { SessionNotReadyError } from './errors.js'; import { ENV_VARS } from './constants.js'; import { SessionManager } from './session-manager.js'; import { HostProxyClient } from './host-proxy-client.js'; import { QueryExecutor } from './query-executor.js'; import { ProgressTracker } from './progress-tracker.js'; import { logger } from '../../utils/logger.js'; /** * Event types emitted by ClaudeCodeService * * @interface ClaudeCodeServiceEvents */ export interface ClaudeCodeServiceEvents { /** * Emitted when a new session is created */ 'session:created': { sessionId: string }; /** * Emitted when a session becomes ready */ 'session:ready': { sessionId: string }; /** * Emitted when a session is terminated */ 'session:terminated': { sessionId: string }; /** * Emitted when task progress is updated */ 'task:progress': ProgressEvent; /** * Emitted when streaming data is received */ 'stream:data': StreamEvent; /** * Emitted when Claude events are received */ 'claude:event': ClaudeEvent; } /** * Main service for interacting with Claude Code CLI * * @class ClaudeCodeService * @extends EventEmitter * * @remarks * This service implements a singleton pattern and provides: * - Session management for Claude instances * - Query execution with streaming support * - Task progress tracking * - Event-driven architecture for real-time updates * - Support for both SDK and host proxy modes */ export class ClaudeCodeService extends EventEmitter { private static instance: ClaudeCodeService; private readonly sessionManager: SessionManager; private readonly hostProxyClient: HostProxyClient; private readonly queryExecutor: QueryExecutor; private progressTracker?: ProgressTracker; private readonly useHostProxy: boolean; /** * Private constructor for singleton pattern * * @private */ private constructor() { super(); this.sessionManager = new SessionManager(); this.hostProxyClient = new HostProxyClient(); this.queryExecutor = new QueryExecutor(); this.useHostProxy = !process.env[ENV_VARS.ANTHROPIC_API_KEY]; logger.info('Claude Code service initialized', { useHostProxy: this.useHostProxy }); } /** * Gets the singleton instance of ClaudeCodeService * * @returns The singleton instance * * @example * ```typescript * const service = ClaudeCodeService.getInstance(); * ``` */ static getInstance(): ClaudeCodeService { if (!ClaudeCodeService.instance) { ClaudeCodeService.instance = new ClaudeCodeService(); } return ClaudeCodeService.instance; } /** * Gets or creates the progress tracker * * @private * @returns The progress tracker instance */ private async getProgressTracker(): Promise<ProgressTracker> { if (!this.progressTracker) { const { TaskStore } = await import('../task-store.js'); this.progressTracker = new ProgressTracker(TaskStore.getInstance()); } return this.progressTracker; } /** * Creates a new session * * @param options - Options for the Claude session * @returns The session ID * * @example * ```typescript * const sessionId = await service.createSession({ * workingDirectory: '/home/user/project', * customSystemPrompt: 'You are a helpful coding assistant' * }); * ``` */ async createSession(options: ClaudeCodeOptions = {}): Promise<string> { const session = this.sessionManager.createSession(options); this.emit('session:created', { sessionId: session.id }); this.emit('session:ready', { sessionId: session.id }); return session.id; } /** * Sets task ID for a session * * @param sessionId - The session ID * @param taskId - The task ID to associate */ setTaskId(sessionId: string, taskId: string): void { this.sessionManager.setTaskId(sessionId, taskId); } /** * Sets MCP session ID for a session * * @param sessionId - The session ID * @param mcpSessionId - The MCP session ID to associate */ setMcpSessionId(sessionId: string, mcpSessionId: string): void { this.sessionManager.setMcpSessionId(sessionId, mcpSessionId); } /** * Finds a session by ID * * @param sessionId - The session ID to find * @returns The session if found, undefined otherwise */ findSession(sessionId: string): ClaudeCodeSession | undefined { return this.sessionManager.findSession(sessionId); } /** * Executes a query synchronously * * @param sessionId - The session ID * @param prompt - The prompt to send to Claude * @param options - Optional query options * @returns The response from Claude * @throws {SessionNotReadyError} If session is not ready * * @remarks * This method will: * 1. Check session readiness * 2. Execute via SDK or host proxy based on configuration * 3. Track progress if a task is associated * 4. Emit appropriate events during execution * * @example * ```typescript * try { * const response = await service.querySync( * sessionId, * 'Implement a REST API endpoint for user creation', * { maxTurns: 20 } * ); * console.log('Claude response:', response); * } catch (error) { * if (error instanceof SessionNotReadyError) { * console.error('Session not ready'); * } * } * ``` */ async querySync( sessionId: string, prompt: string, options?: Partial<ClaudeCodeOptions> ): Promise<string> { const session = this.sessionManager.getSession(sessionId); logger.info('Query requested', { sessionId, status: session.status, promptLength: prompt.length, workingDirectory: session.workingDirectory, useHostProxy: this.useHostProxy }); if (session.status !== 'ready' && session.status !== 'busy') { throw new SessionNotReadyError(sessionId, session.status); } this.sessionManager.updateStatus(sessionId, 'busy'); try { let result: string; if (this.useHostProxy) { result = await this.executeViaHostProxy(session, prompt); } else { const queryResult = await this.queryExecutor.execute(session, prompt, options); result = queryResult.content; if (session.taskId) { const tracker = await this.getProgressTracker(); for (const message of queryResult.messages) { if (message.type === 'assistant' && message.message?.content) { await tracker.logAssistantMessage(session.taskId, message.message.content, session.mcpSessionId); } } } } this.sessionManager.updateStatus(sessionId, 'ready'); return result; } catch (error) { this.sessionManager.updateStatus(sessionId, 'error'); session.errorBuffer.push(error instanceof Error ? error.message : String(error)); throw error; } } /** * Gets the task store instance * * @private * @returns The task store instance */ private async getTaskStore() { const { TaskStore } = await import('../task-store.js'); return TaskStore.getInstance(); } /** * Executes via host proxy * * @private * @param session - The Claude session * @param prompt - The prompt to execute * @returns The response from Claude * * @remarks * This method handles execution through the host proxy when * ANTHROPIC_API_KEY is not available. It: * 1. Sets up environment variables for the Claude process * 2. Executes the command via the host proxy * 3. Handles streaming data and events * 4. Tracks progress if a task is associated */ private async executeViaHostProxy( session: ClaudeCodeSession, prompt: string ): Promise<string> { const tracker = await this.getProgressTracker(); const env: Record<string, string> = {}; if (session.taskId) { const { AgentManager } = await import('../agent-manager/index.js'); const agentManager = AgentManager.getInstance(); const sessions = agentManager.getAllSessions(); const agentSession = sessions.find(s => s.taskId === session.taskId); if (agentSession) { env.CLAUDE_SESSION_ID = agentSession.id; logger.info('Setting CLAUDE_SESSION_ID for task', { taskId: session.taskId, sessionId: agentSession.id }); } } const rawResult = await this.hostProxyClient.execute( prompt, session.workingDirectory, async (data: string) => { session.streamBuffer.push(data); this.emit('stream:data', { sessionId: session.id, data, taskId: session.taskId }); if (session.taskId) { await tracker.parseProgressFromStream(session, data); } }, env, session.id, session.taskId, (event: ClaudeEvent) => { this.emit('claude:event', event); if (session.taskId) { this.logEventToTask(session, event); } } ); // Parse the JSON result from Claude let parsedResult: any; let textResult = rawResult; try { parsedResult = JSON.parse(rawResult); if (parsedResult.type === 'result' && parsedResult.result) { textResult = parsedResult.result; // Log the parsed result to the task if (session.taskId) { const taskStore = await this.getTaskStore(); await taskStore.addLog(session.taskId, { timestamp: new Date().toISOString(), level: parsedResult.is_error ? 'error' : 'info', type: 'output', prefix: 'CLAUDE_RESULT', message: textResult, metadata: { success: !parsedResult.is_error, duration: parsedResult.duration_ms, apiDuration: parsedResult.duration_api_ms, turns: parsedResult.num_turns, sessionId: parsedResult.session_id, cost: parsedResult.total_cost_usd, usage: parsedResult.usage ? { inputTokens: parsedResult.usage.input_tokens || 0, outputTokens: parsedResult.usage.output_tokens || 0, cacheCreationTokens: parsedResult.usage.cache_creation_input_tokens || 0, cacheReadTokens: parsedResult.usage.cache_read_input_tokens || 0, totalTokens: (parsedResult.usage.input_tokens || 0) + (parsedResult.usage.output_tokens || 0) + (parsedResult.usage.cache_creation_input_tokens || 0) + (parsedResult.usage.cache_read_input_tokens || 0) } : undefined } }, session.mcpSessionId); } } } catch (e) { // If parsing fails, use the raw result as text logger.warn('Failed to parse Claude JSON result, using raw output', { error: e }); } const sdkMessage = { type: 'assistant' as const, message: { content: [{ type: 'text', text: textResult }] }, session_id: session.id, parent_tool_use_id: null }; session.outputBuffer.push(sdkMessage as SDKMessage); return textResult; } /** * Ends a session * * @param sessionId - The session ID to end */ async endSession(sessionId: string): Promise<void> { this.sessionManager.endSession(sessionId); this.emit('session:terminated', { sessionId }); } /** * Gets a session * * @param sessionId - The session ID to retrieve * @returns The session if found, undefined otherwise */ getSession(sessionId: string): ClaudeCodeSession | undefined { return this.sessionManager.findSession(sessionId); } /** * Gets all sessions * * @returns Array of all Claude sessions */ getAllSessions(): ClaudeCodeSession[] { return this.sessionManager.getAllSessions(); } /** * Gets service metrics * * @returns Service metrics including session counts */ getMetrics() { return this.sessionManager.getMetrics(); } /** * Cleans up old sessions * * @param maxAgeMs - Maximum age in milliseconds * @returns Number of sessions cleaned up */ cleanupOldSessions(maxAgeMs?: number): number { return this.sessionManager.cleanupOldSessions(maxAgeMs); } /** * Type-safe event emitter * * @template K - Event name * @param event - The event to emit * @param args - Event arguments * @returns True if event had listeners */ emit<K extends keyof ClaudeCodeServiceEvents>( event: K, ...args: ClaudeCodeServiceEvents[K] extends void ? [] : [ClaudeCodeServiceEvents[K]] ): boolean { return super.emit(event, ...args); } /** * Type-safe event listener registration * * @template K - Event name * @param event - The event to listen for * @param listener - The callback function * @returns This instance for chaining */ on<K extends keyof ClaudeCodeServiceEvents>( event: K, listener: (arg: ClaudeCodeServiceEvents[K]) => void ): this { return super.on(event, listener); } /** * Type-safe event listener removal * * @template K - Event name * @param event - The event to stop listening for * @param listener - The callback function to remove * @returns This instance for chaining */ off<K extends keyof ClaudeCodeServiceEvents>( event: K, listener: (arg: ClaudeCodeServiceEvents[K]) => void ): this { return super.off(event, listener); } /** * Log Claude events to task * * @private * @param session - The Claude session * @param event - The Claude event to log * * @remarks * Maps Claude event types to task log entries with appropriate * log levels and types. Skips stream events to avoid verbosity. */ private async logEventToTask(session: ClaudeCodeSession, event: ClaudeEvent): Promise<void> { const { TaskStore } = await import('../task-store.js'); const taskStore = TaskStore.getInstance(); const eventTypeMap: Record<ClaudeEventType, { logType: string; level: string; prefix?: string }> = { [ClaudeEventType.ProcessStart]: { logType: 'system', level: 'info', prefix: 'PROCESS_START' }, [ClaudeEventType.ProcessEnd]: { logType: 'system', level: 'info', prefix: 'PROCESS_END' }, [ClaudeEventType.ToolStart]: { logType: 'tool', level: 'info', prefix: 'TOOL_START' }, [ClaudeEventType.ToolEnd]: { logType: 'tool', level: 'info', prefix: 'TOOL_END' }, [ClaudeEventType.Message]: { logType: 'agent', level: 'info' }, [ClaudeEventType.Stream]: { logType: 'output', level: 'debug' }, [ClaudeEventType.Error]: { logType: 'error', level: 'error', prefix: 'ERROR' }, [ClaudeEventType.Result]: { logType: 'output', level: 'info', prefix: 'RESULT' }, }; const mapping = eventTypeMap[event.type]; if (!mapping) return; if (event.type === ClaudeEventType.Stream) return; let level = mapping.level; if (event.type === ClaudeEventType.ProcessEnd && event.metadata?.exitCode !== 0) { level = 'error'; } if (event.type === ClaudeEventType.ToolEnd && event.metadata?.success === false) { level = 'error'; } await taskStore.addLog(session.taskId!, { timestamp: event.timestamp, level: level as any, type: mapping.logType as any, prefix: mapping.prefix, message: event.content, metadata: event.metadata }, session.mcpSessionId); } }

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/systempromptio/systemprompt-code-orchestrator'

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