Files
automaker/docs/server/utilities.md
SuperComboGamer 8d578558ff style: fix formatting with Prettier
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 20:31:57 -05:00

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

  1. Image Handler
  2. Prompt Builder
  3. Model Resolver
  4. Conversation Utils
  5. Error Handler
  6. Subprocess Manager
  7. Events
  8. Auth
  9. 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, .jpegimage/jpeg
  • .pngimage/png
  • .gifimage/gif
  • .webpimage/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 paths
  • workDir - 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 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

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: 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.

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:

  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:

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

  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:

// ✅ 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:

describe('image-handler', () => {
  it('should detect MIME type correctly', () => {
    expect(getMimeTypeForImage('photo.jpg')).toBe('image/jpeg');
    expect(getMimeTypeForImage('diagram.png')).toBe('image/png');
  });
});