🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
16 KiB
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
- Image Handler
- Prompt Builder
- Model Resolver
- Conversation Utils
- Error Handler
- Subprocess Manager
- Events
- Auth
- 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:
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
interface ImageData {
base64: string; // Base64-encoded image data
mimeType: string; // MIME type
filename: string; // File basename
originalPath: string; // Original file path
}
Example:
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 pathsworkDir- Optional working directory for resolving relative paths
Returns: Array of ImageContentBlock
interface ImageContentBlock {
type: 'image';
source: {
type: 'base64';
media_type: string;
data: string;
};
}
Example:
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:
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 promptimagePaths- Optional array of image file pathsworkDir- Optional working directory for resolving relative pathsincludeImagePaths- Whether to append image paths to the text (default: false)
Returns: PromptWithImages
interface PromptWithImages {
content: PromptContent; // string | Array<ContentBlock>
hasImages: boolean;
}
type PromptContent =
| string
| Array<{
type: string;
text?: string;
source?: object;
}>;
Example:
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: trueto list paths for Read tool access - AutoModeService: Set
includeImagePaths: falseto 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.
export const CLAUDE_MODEL_MAP: Record<string, string> = {
haiku: 'claude-haiku-4-5',
sonnet: 'claude-sonnet-4-20250514',
opus: 'claude-opus-4-5-20251101',
} as const;
DEFAULT_MODELS
Default models per provider.
export const DEFAULT_MODELS = {
claude: 'claude-opus-4-5-20251101',
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:
- If
modelKeyis undefined → returndefaultModel - If starts with
"gpt-"or"o"→ pass through (OpenAI/Codex model) - If includes
"claude-"→ pass through (full Claude model string) - If in
CLAUDE_MODEL_MAP→ return mapped value - Otherwise → return
defaultModelwith warning
Example:
import { resolveModelString, DEFAULT_MODELS } from '../lib/model-resolver.js';
resolveModelString('opus');
// Returns: "claude-opus-4-5-20251101"
// Logs: "[ModelResolver] Resolved model alias: "opus" -> "claude-opus-4-5-20251101""
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-5-20251101"
// Logs: "[ModelResolver] Unknown model key "invalid-model", using default: "claude-opus-4-5-20251101""
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:
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
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:
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:
// 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:
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:
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
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:
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:
if (isAuthenticationError(error.message)) {
console.error('Please check your API key configuration');
}
classifyError(error: unknown): ErrorInfo
Classify an error into a specific type.
Example:
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:
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
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:
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:
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
-
Image handling → Always use
image-handler.tsutilities- ✅ Do:
convertImagesToContentBlocks(imagePaths, workDir) - ❌ Don't: Manually read files and encode base64
- ✅ Do:
-
Prompt building → Use
prompt-builder.tsfor consistency- ✅ Do:
buildPromptWithImages(text, images, workDir, includePathsInText) - ❌ Don't: Manually construct content block arrays
- ✅ Do:
-
Model resolution → Use
model-resolver.tsfor all model handling- ✅ Do:
resolveModelString(feature.model, DEFAULT_MODELS.claude) - ❌ Don't: Inline model mapping logic
- ✅ Do:
-
Error handling → Use
error-handler.tsfor classification- ✅ Do:
if (isAbortError(error)) { ... } - ❌ Don't:
if (error instanceof AbortError || error.name === "AbortError") { ... }
- ✅ Do:
Importing Utilities
Always use .js extension in imports for ESM compatibility:
// ✅ Correct
import { buildPromptWithImages } from '../lib/prompt-builder.js';
// ❌ Incorrect
import { buildPromptWithImages } from '../lib/prompt-builder';
Testing Utilities
When writing tests for utilities:
- Unit tests - Test each function in isolation
- Integration tests - Test utilities working together
- Mock external dependencies - File system, child processes
Example:
describe('image-handler', () => {
it('should detect MIME type correctly', () => {
expect(getMimeTypeForImage('photo.jpg')).toBe('image/jpeg');
expect(getMimeTypeForImage('diagram.png')).toBe('image/png');
});
});