| # Server Utilities Reference |
|
|
| This document describes all utility modules available in `apps/server/src/lib/`. These utilities provide reusable functionality for image handling, prompt building, model resolution, conversation management, and error handling. |
|
|
| --- |
|
|
| ## Table of Contents |
|
|
| 1. [Image Handler](#image-handler) |
| 2. [Prompt Builder](#prompt-builder) |
| 3. [Model Resolver](#model-resolver) |
| 4. [Conversation Utils](#conversation-utils) |
| 5. [Error Handler](#error-handler) |
| 6. [Subprocess Manager](#subprocess-manager) |
| 7. [Events](#events) |
| 8. [Auth](#auth) |
| 9. [Security](#security) |
|
|
| --- |
|
|
| ## Image Handler |
|
|
| **Location**: `apps/server/src/lib/image-handler.ts` |
|
|
| Centralized utilities for processing image files, including MIME type detection, base64 encoding, and content block generation for Claude SDK format. |
|
|
| ### Functions |
|
|
| #### `getMimeTypeForImage(imagePath: string): string` |
|
|
| Get MIME type for an image file based on its extension. |
|
|
| **Supported formats**: |
|
|
| - `.jpg`, `.jpeg` β `image/jpeg` |
| - `.png` β `image/png` |
| - `.gif` β `image/gif` |
| - `.webp` β `image/webp` |
| - Default: `image/png` |
|
|
| **Example**: |
|
|
| ```typescript |
| import { getMimeTypeForImage } from '../lib/image-handler.js'; |
| |
| const mimeType = getMimeTypeForImage('/path/to/image.jpg'); |
| // Returns: "image/jpeg" |
| ``` |
|
|
| --- |
|
|
| #### `readImageAsBase64(imagePath: string): Promise<ImageData>` |
|
|
| Read an image file and convert to base64 with metadata. |
|
|
| **Returns**: `ImageData` |
|
|
| ```typescript |
| interface ImageData { |
| base64: string; // Base64-encoded image data |
| mimeType: string; // MIME type |
| filename: string; // File basename |
| originalPath: string; // Original file path |
| } |
| ``` |
|
|
| **Example**: |
|
|
| ```typescript |
| const imageData = await readImageAsBase64('/path/to/photo.png'); |
| console.log(imageData.base64); // "iVBORw0KG..." |
| console.log(imageData.mimeType); // "image/png" |
| console.log(imageData.filename); // "photo.png" |
| ``` |
|
|
| --- |
|
|
| #### `convertImagesToContentBlocks(imagePaths: string[], workDir?: string): Promise<ImageContentBlock[]>` |
|
|
| Convert image paths to content blocks in Claude SDK format. Handles both relative and absolute paths. |
|
|
| **Parameters**: |
|
|
| - `imagePaths` - Array of image file paths |
| - `workDir` - Optional working directory for resolving relative paths |
|
|
| **Returns**: Array of `ImageContentBlock` |
|
|
| ```typescript |
| interface ImageContentBlock { |
| type: 'image'; |
| source: { |
| type: 'base64'; |
| media_type: string; |
| data: string; |
| }; |
| } |
| ``` |
|
|
| **Example**: |
|
|
| ```typescript |
| const imageBlocks = await convertImagesToContentBlocks( |
| ['./screenshot.png', '/absolute/path/diagram.jpg'], |
| '/project/root' |
| ); |
| |
| // Use in prompt content |
| const contentBlocks = [{ type: 'text', text: 'Analyze these images:' }, ...imageBlocks]; |
| ``` |
|
|
| --- |
|
|
| #### `formatImagePathsForPrompt(imagePaths: string[]): string` |
|
|
| Format image paths as a bulleted list for inclusion in text prompts. |
|
|
| **Returns**: Formatted string with image paths, or empty string if no images. |
|
|
| **Example**: |
|
|
| ```typescript |
| const pathsList = formatImagePathsForPrompt([ |
| '/screenshots/login.png', |
| '/diagrams/architecture.png', |
| ]); |
| |
| // Returns: |
| // "\n\nAttached images:\n- /screenshots/login.png\n- /diagrams/architecture.png\n" |
| ``` |
|
|
| --- |
|
|
| ## Prompt Builder |
|
|
| **Location**: `apps/server/src/lib/prompt-builder.ts` |
|
|
| Standardized prompt building that combines text prompts with image attachments. |
|
|
| ### Functions |
|
|
| #### `buildPromptWithImages(basePrompt: string, imagePaths?: string[], workDir?: string, includeImagePaths: boolean = false): Promise<PromptWithImages>` |
|
|
| Build a prompt with optional image attachments. |
|
|
| **Parameters**: |
|
|
| - `basePrompt` - The text prompt |
| - `imagePaths` - Optional array of image file paths |
| - `workDir` - Optional working directory for resolving relative paths |
| - `includeImagePaths` - Whether to append image paths to the text (default: false) |
|
|
| **Returns**: `PromptWithImages` |
|
|
| ```typescript |
| interface PromptWithImages { |
| content: PromptContent; // string | Array<ContentBlock> |
| hasImages: boolean; |
| } |
| |
| type PromptContent = |
| | string |
| | Array<{ |
| type: string; |
| text?: string; |
| source?: object; |
| }>; |
| ``` |
|
|
| **Example**: |
|
|
| ```typescript |
| import { buildPromptWithImages } from '../lib/prompt-builder.js'; |
| |
| // Without images |
| const { content } = await buildPromptWithImages('What is 2+2?'); |
| // content: "What is 2+2?" (simple string) |
| |
| // With images |
| const { content, hasImages } = await buildPromptWithImages( |
| 'Analyze this screenshot', |
| ['/path/to/screenshot.png'], |
| '/project/root', |
| true // include image paths in text |
| ); |
| // content: [ |
| // { type: "text", text: "Analyze this screenshot\n\nAttached images:\n- /path/to/screenshot.png\n" }, |
| // { type: "image", source: { type: "base64", media_type: "image/png", data: "..." } } |
| // ] |
| // hasImages: true |
| ``` |
|
|
| **Use Cases**: |
|
|
| - **AgentService**: Set `includeImagePaths: true` to list paths for Read tool access |
| - **AutoModeService**: Set `includeImagePaths: false` to avoid duplication in feature descriptions |
|
|
| --- |
|
|
| ## Model Resolver |
|
|
| **Location**: `apps/server/src/lib/model-resolver.ts` |
|
|
| Centralized model string mapping and resolution for handling model aliases and provider detection. |
|
|
| ### Constants |
|
|
| #### `CLAUDE_MODEL_MAP` |
|
|
| Model alias mapping for Claude models. |
|
|
| ```typescript |
| export const CLAUDE_MODEL_MAP: Record<string, string> = { |
| haiku: 'claude-haiku-4-5', |
| sonnet: 'claude-sonnet-4-20250514', |
| opus: 'claude-opus-4-6', |
| } as const; |
| ``` |
|
|
| #### `DEFAULT_MODELS` |
| |
| Default models per provider. |
| |
| ```typescript |
| export const DEFAULT_MODELS = { |
| claude: 'claude-opus-4-6', |
| openai: 'gpt-5.2', |
| } as const; |
| ``` |
| |
| ### Functions |
| |
| #### `resolveModelString(modelKey?: string, defaultModel: string = DEFAULT_MODELS.claude): string` |
| |
| Resolve a model key/alias to a full model string. |
| |
| **Logic**: |
| |
| 1. If `modelKey` is undefined β return `defaultModel` |
| 2. If starts with `"gpt-"` or `"o"` β pass through (OpenAI/Codex model) |
| 3. If includes `"claude-"` β pass through (full Claude model string) |
| 4. If in `CLAUDE_MODEL_MAP` β return mapped value |
| 5. Otherwise β return `defaultModel` with warning |
| |
| **Example**: |
| |
| ```typescript |
| import { resolveModelString, DEFAULT_MODELS } from '../lib/model-resolver.js'; |
| |
| resolveModelString('opus'); |
| // Returns: "claude-opus-4-6" |
| // Logs: "[ModelResolver] Resolved model alias: "opus" -> "claude-opus-4-6"" |
| |
| resolveModelString('gpt-5.2'); |
| // Returns: "gpt-5.2" |
| // Logs: "[ModelResolver] Using OpenAI/Codex model: gpt-5.2" |
| |
| resolveModelString('claude-sonnet-4-20250514'); |
| // Returns: "claude-sonnet-4-20250514" |
| // Logs: "[ModelResolver] Using full Claude model string: claude-sonnet-4-20250514" |
| |
| resolveModelString('invalid-model'); |
| // Returns: "claude-opus-4-6" |
| // Logs: "[ModelResolver] Unknown model key "invalid-model", using default: "claude-opus-4-6"" |
| ``` |
| |
| --- |
| |
| #### `getEffectiveModel(explicitModel?: string, sessionModel?: string, defaultModel?: string): string` |
| |
| Get the effective model from multiple sources with priority. |
| |
| **Priority**: explicit model > session model > default model |
| |
| **Example**: |
| |
| ```typescript |
| import { getEffectiveModel } from '../lib/model-resolver.js'; |
| |
| // Explicit model takes precedence |
| getEffectiveModel('sonnet', 'opus'); |
| // Returns: "claude-sonnet-4-20250514" |
| |
| // Falls back to session model |
| getEffectiveModel(undefined, 'haiku'); |
| // Returns: "claude-haiku-4-5" |
| |
| // Falls back to default |
| getEffectiveModel(undefined, undefined, 'gpt-5.2'); |
| // Returns: "gpt-5.2" |
| ``` |
| |
| --- |
| |
| ## Conversation Utils |
| |
| **Location**: `apps/server/src/lib/conversation-utils.ts` |
| |
| Standardized conversation history processing for both SDK-based and CLI-based providers. |
| |
| ### Types |
| |
| ```typescript |
| import type { ConversationMessage } from '../providers/types.js'; |
| |
| interface ConversationMessage { |
| role: 'user' | 'assistant'; |
| content: string | Array<{ type: string; text?: string; source?: object }>; |
| } |
| ``` |
| |
| ### Functions |
| |
| #### `extractTextFromContent(content: string | Array<ContentBlock>): string` |
| |
| Extract plain text from message content (handles both string and array formats). |
| |
| **Example**: |
| |
| ```typescript |
| import { extractTextFromContent } from "../lib/conversation-utils.js"; |
| |
| // String content |
| extractTextFromContent("Hello world"); |
| // Returns: "Hello world" |
| |
| // Array content |
| extractTextFromContent([ |
| { type: "text", text: "Hello" }, |
| { type: "image", source: {...} }, |
| { type: "text", text: "world" } |
| ]); |
| // Returns: "Hello\nworld" |
| ``` |
| |
| --- |
| |
| #### `normalizeContentBlocks(content: string | Array<ContentBlock>): Array<ContentBlock>` |
| |
| Normalize message content to array format. |
| |
| **Example**: |
| |
| ```typescript |
| // String β array |
| normalizeContentBlocks('Hello'); |
| // Returns: [{ type: "text", text: "Hello" }] |
| |
| // Array β pass through |
| normalizeContentBlocks([{ type: 'text', text: 'Hello' }]); |
| // Returns: [{ type: "text", text: "Hello" }] |
| ``` |
| |
| --- |
| |
| #### `formatHistoryAsText(history: ConversationMessage[]): string` |
| |
| Format conversation history as plain text for CLI-based providers (e.g., Codex). |
| |
| **Returns**: Formatted text with role labels, or empty string if no history. |
| |
| **Example**: |
| |
| ```typescript |
| const history = [ |
| { role: 'user', content: 'What is 2+2?' }, |
| { role: 'assistant', content: '2+2 equals 4.' }, |
| ]; |
| |
| const formatted = formatHistoryAsText(history); |
| // Returns: |
| // "Previous conversation: |
| // |
| // User: What is 2+2? |
| // |
| // Assistant: 2+2 equals 4. |
| // |
| // --- |
| // |
| // " |
| ``` |
| |
| --- |
| |
| #### `convertHistoryToMessages(history: ConversationMessage[]): Array<SDKMessage>` |
| |
| Convert conversation history to Claude SDK message format. |
| |
| **Returns**: Array of SDK-formatted messages ready to yield in async generator. |
| |
| **Example**: |
| |
| ```typescript |
| const history = [ |
| { role: 'user', content: 'Hello' }, |
| { role: 'assistant', content: 'Hi there!' }, |
| ]; |
| |
| const messages = convertHistoryToMessages(history); |
| // Returns: |
| // [ |
| // { |
| // type: "user", |
| // session_id: "", |
| // message: { |
| // role: "user", |
| // content: [{ type: "text", text: "Hello" }] |
| // }, |
| // parent_tool_use_id: null |
| // }, |
| // { |
| // type: "assistant", |
| // session_id: "", |
| // message: { |
| // role: "assistant", |
| // content: [{ type: "text", text: "Hi there!" }] |
| // }, |
| // parent_tool_use_id: null |
| // } |
| // ] |
| ``` |
| |
| --- |
| |
| ## Error Handler |
| |
| **Location**: `apps/server/src/lib/error-handler.ts` |
| |
| Standardized error classification and handling utilities. |
| |
| ### Types |
| |
| ```typescript |
| export type ErrorType = 'authentication' | 'abort' | 'execution' | 'unknown'; |
| |
| export interface ErrorInfo { |
| type: ErrorType; |
| message: string; |
| isAbort: boolean; |
| isAuth: boolean; |
| originalError: unknown; |
| } |
| ``` |
| |
| ### Functions |
| |
| #### `isAbortError(error: unknown): boolean` |
| |
| Check if an error is an abort/cancellation error. |
| |
| **Example**: |
| |
| ```typescript |
| import { isAbortError } from '../lib/error-handler.js'; |
| |
| try { |
| // ... operation |
| } catch (error) { |
| if (isAbortError(error)) { |
| console.log('Operation was cancelled'); |
| return { success: false, aborted: true }; |
| } |
| } |
| ``` |
| |
| --- |
| |
| #### `isAuthenticationError(errorMessage: string): boolean` |
| |
| Check if an error is an authentication/API key error. |
| |
| **Detects**: |
| |
| - "Authentication failed" |
| - "Invalid API key" |
| - "authentication_failed" |
| - "Fix external API key" |
|
|
| **Example**: |
|
|
| ```typescript |
| if (isAuthenticationError(error.message)) { |
| console.error('Please check your API key configuration'); |
| } |
| ``` |
|
|
| --- |
|
|
| #### `classifyError(error: unknown): ErrorInfo` |
|
|
| Classify an error into a specific type. |
|
|
| **Example**: |
|
|
| ```typescript |
| import { classifyError } from '../lib/error-handler.js'; |
| |
| try { |
| // ... operation |
| } catch (error) { |
| const errorInfo = classifyError(error); |
| |
| switch (errorInfo.type) { |
| case 'authentication': |
| // Handle auth errors |
| break; |
| case 'abort': |
| // Handle cancellation |
| break; |
| case 'execution': |
| // Handle other errors |
| break; |
| } |
| } |
| ``` |
|
|
| --- |
|
|
| #### `getUserFriendlyErrorMessage(error: unknown): string` |
|
|
| Get a user-friendly error message. |
|
|
| **Example**: |
|
|
| ```typescript |
| try { |
| // ... operation |
| } catch (error) { |
| const friendlyMessage = getUserFriendlyErrorMessage(error); |
| // "Operation was cancelled" for abort errors |
| // "Authentication failed. Please check your API key." for auth errors |
| // Original error message for other errors |
| } |
| ``` |
|
|
| --- |
|
|
| ## Subprocess Manager |
|
|
| **Location**: `apps/server/src/lib/subprocess-manager.ts` |
|
|
| Utilities for spawning CLI processes and parsing JSONL streams (used by Codex provider). |
|
|
| ### Types |
|
|
| ```typescript |
| export interface SubprocessOptions { |
| command: string; |
| args: string[]; |
| cwd: string; |
| env?: Record<string, string>; |
| abortController?: AbortController; |
| timeout?: number; // Milliseconds of no output before timeout |
| } |
| |
| export interface SubprocessResult { |
| stdout: string; |
| stderr: string; |
| exitCode: number | null; |
| } |
| ``` |
|
|
| ### Functions |
|
|
| #### `async function* spawnJSONLProcess(options: SubprocessOptions): AsyncGenerator<unknown>` |
|
|
| Spawns a subprocess and streams JSONL output line-by-line. |
|
|
| **Features**: |
|
|
| - Parses each line as JSON |
| - Handles abort signals |
| - 30-second timeout detection for hanging processes |
| - Collects stderr for error reporting |
| - Continues processing other lines if one fails to parse |
|
|
| **Example**: |
|
|
| ```typescript |
| import { spawnJSONLProcess } from '../lib/subprocess-manager.js'; |
| |
| const stream = spawnJSONLProcess({ |
| command: 'codex', |
| args: ['exec', '--model', 'gpt-5.2', '--json', '--full-auto', 'Fix the bug'], |
| cwd: '/project/path', |
| env: { OPENAI_API_KEY: 'sk-...' }, |
| abortController: new AbortController(), |
| timeout: 30000, |
| }); |
| |
| for await (const event of stream) { |
| console.log('Received event:', event); |
| // Process JSONL events |
| } |
| ``` |
|
|
| --- |
|
|
| #### `async function spawnProcess(options: SubprocessOptions): Promise<SubprocessResult>` |
|
|
| Spawns a subprocess and collects all output. |
|
|
| **Example**: |
|
|
| ```typescript |
| const result = await spawnProcess({ |
| command: 'git', |
| args: ['status'], |
| cwd: '/project/path', |
| }); |
| |
| console.log(result.stdout); // Git status output |
| console.log(result.exitCode); // 0 for success |
| ``` |
|
|
| --- |
|
|
| ## Events |
|
|
| **Location**: `apps/server/src/lib/events.ts` |
|
|
| Event emitter system for WebSocket communication. |
|
|
| **Documented separately** - see existing codebase for event types and usage. |
|
|
| --- |
|
|
| ## Auth |
|
|
| **Location**: `apps/server/src/lib/auth.ts` |
|
|
| Authentication utilities for API endpoints. |
|
|
| **Documented separately** - see existing codebase for authentication flow. |
|
|
| --- |
|
|
| ## Security |
|
|
| **Location**: `apps/server/src/lib/security.ts` |
|
|
| Security utilities for input validation and sanitization. |
|
|
| **Documented separately** - see existing codebase for security patterns. |
|
|
| --- |
|
|
| ## Best Practices |
|
|
| ### When to Use Which Utility |
|
|
| 1. **Image handling** β Always use `image-handler.ts` utilities |
| - β
Do: `convertImagesToContentBlocks(imagePaths, workDir)` |
| - β Don't: Manually read files and encode base64 |
|
|
| 2. **Prompt building** β Use `prompt-builder.ts` for consistency |
| - β
Do: `buildPromptWithImages(text, images, workDir, includePathsInText)` |
| - β Don't: Manually construct content block arrays |
|
|
| 3. **Model resolution** β Use `model-resolver.ts` for all model handling |
| - β
Do: `resolveModelString(feature.model, DEFAULT_MODELS.claude)` |
| - β Don't: Inline model mapping logic |
|
|
| 4. **Error handling** β Use `error-handler.ts` for classification |
| - β
Do: `if (isAbortError(error)) { ... }` |
| - β Don't: `if (error instanceof AbortError || error.name === "AbortError") { ... }` |
|
|
| ### Importing Utilities |
|
|
| Always use `.js` extension in imports for ESM compatibility: |
|
|
| ```typescript |
| // β
Correct |
| import { buildPromptWithImages } from '../lib/prompt-builder.js'; |
| |
| // β Incorrect |
| import { buildPromptWithImages } from '../lib/prompt-builder'; |
| ``` |
|
|
| --- |
|
|
| ## Testing Utilities |
|
|
| When writing tests for utilities: |
|
|
| 1. **Unit tests** - Test each function in isolation |
| 2. **Integration tests** - Test utilities working together |
| 3. **Mock external dependencies** - File system, child processes |
|
|
| Example: |
|
|
| ```typescript |
| describe('image-handler', () => { |
| it('should detect MIME type correctly', () => { |
| expect(getMimeTypeForImage('photo.jpg')).toBe('image/jpeg'); |
| expect(getMimeTypeForImage('diagram.png')).toBe('image/png'); |
| }); |
| }); |
| ``` |
|
|