# 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-5-20251101',
} as const;
```

#### `DEFAULT_MODELS`

Default models per provider.

```typescript
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**:

```typescript
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**:

```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');
  });
});
```
