Files
automaker/docs/server/utilities.md
Kacper 7cbdb3db73 refactor: eliminate code duplication with shared utilities
Created 5 new utility modules in apps/server/src/lib/ to eliminate ~320 lines of duplicated code:
- image-handler.ts: Centralized image processing (MIME types, base64, content blocks)
- prompt-builder.ts: Standardized prompt building with image attachments
- model-resolver.ts: Model alias resolution and provider routing
- conversation-utils.ts: Conversation history processing for providers
- error-handler.ts: Error classification and user-friendly messages

Updated services and providers to use shared utilities:
- agent-service.ts: -51 lines (removed duplicate image handling, model logic)
- auto-mode-service.ts: -75 lines (removed MODEL_MAP, duplicate utilities)
- claude-provider.ts: -10 lines (uses conversation-utils)
- codex-provider.ts: -5 lines (uses conversation-utils)

Added comprehensive documentation:
- docs/server/utilities.md: Complete reference for all 9 lib utilities
- docs/server/providers.md: Provider architecture guide with examples

Benefits:
- Single source of truth for critical business logic
- Improved maintainability and testability
- Consistent behavior across services and providers
- Better documentation for future development

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 04:26:58 +01:00

673 lines
16 KiB
Markdown

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