mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge branch 'main' of github.com:AutoMaker-Org/automaker into improve-context-page
This commit is contained in:
@@ -19,15 +19,15 @@ import type { ConversationMessage } from '@automaker/types';
|
||||
export function extractTextFromContent(
|
||||
content: string | Array<{ type: string; text?: string; source?: object }>
|
||||
): string {
|
||||
if (typeof content === "string") {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Extract text blocks only
|
||||
return content
|
||||
.filter((block) => block.type === "text")
|
||||
.map((block) => block.text || "")
|
||||
.join("\n");
|
||||
.filter((block) => block.type === 'text')
|
||||
.map((block) => block.text || '')
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ export function normalizeContentBlocks(
|
||||
if (Array.isArray(content)) {
|
||||
return content;
|
||||
}
|
||||
return [{ type: "text", text: content }];
|
||||
return [{ type: 'text', text: content }];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,18 +53,18 @@ export function normalizeContentBlocks(
|
||||
*/
|
||||
export function formatHistoryAsText(history: ConversationMessage[]): string {
|
||||
if (history.length === 0) {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
let historyText = "Previous conversation:\n\n";
|
||||
let historyText = 'Previous conversation:\n\n';
|
||||
|
||||
for (const msg of history) {
|
||||
const contentText = extractTextFromContent(msg.content);
|
||||
const role = msg.role === "user" ? "User" : "Assistant";
|
||||
const role = msg.role === 'user' ? 'User' : 'Assistant';
|
||||
historyText += `${role}: ${contentText}\n\n`;
|
||||
}
|
||||
|
||||
historyText += "---\n\n";
|
||||
historyText += '---\n\n';
|
||||
return historyText;
|
||||
}
|
||||
|
||||
@@ -74,20 +74,18 @@ export function formatHistoryAsText(history: ConversationMessage[]): string {
|
||||
* @param history - Array of conversation messages
|
||||
* @returns Array of Claude SDK formatted messages
|
||||
*/
|
||||
export function convertHistoryToMessages(
|
||||
history: ConversationMessage[]
|
||||
): Array<{
|
||||
type: "user" | "assistant";
|
||||
export function convertHistoryToMessages(history: ConversationMessage[]): Array<{
|
||||
type: 'user' | 'assistant';
|
||||
session_id: string;
|
||||
message: {
|
||||
role: "user" | "assistant";
|
||||
role: 'user' | 'assistant';
|
||||
content: Array<{ type: string; text?: string; source?: object }>;
|
||||
};
|
||||
parent_tool_use_id: null;
|
||||
}> {
|
||||
return history.map((historyMsg) => ({
|
||||
type: historyMsg.role,
|
||||
session_id: "",
|
||||
session_id: '',
|
||||
message: {
|
||||
role: historyMsg.role,
|
||||
content: normalizeContentBlocks(historyMsg.content),
|
||||
|
||||
@@ -17,10 +17,7 @@ import type { ErrorType, ErrorInfo } from '@automaker/types';
|
||||
* @returns True if the error is an abort error
|
||||
*/
|
||||
export function isAbortError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
(error.name === "AbortError" || error.message.includes("abort"))
|
||||
);
|
||||
return error instanceof Error && (error.name === 'AbortError' || error.message.includes('abort'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,10 +29,10 @@ export function isAbortError(error: unknown): boolean {
|
||||
export function isCancellationError(errorMessage: string): boolean {
|
||||
const lowerMessage = errorMessage.toLowerCase();
|
||||
return (
|
||||
lowerMessage.includes("cancelled") ||
|
||||
lowerMessage.includes("canceled") ||
|
||||
lowerMessage.includes("stopped") ||
|
||||
lowerMessage.includes("aborted")
|
||||
lowerMessage.includes('cancelled') ||
|
||||
lowerMessage.includes('canceled') ||
|
||||
lowerMessage.includes('stopped') ||
|
||||
lowerMessage.includes('aborted')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,10 +44,10 @@ export function isCancellationError(errorMessage: string): boolean {
|
||||
*/
|
||||
export function isAuthenticationError(errorMessage: string): boolean {
|
||||
return (
|
||||
errorMessage.includes("Authentication failed") ||
|
||||
errorMessage.includes("Invalid API key") ||
|
||||
errorMessage.includes("authentication_failed") ||
|
||||
errorMessage.includes("Fix external API key")
|
||||
errorMessage.includes('Authentication failed') ||
|
||||
errorMessage.includes('Invalid API key') ||
|
||||
errorMessage.includes('authentication_failed') ||
|
||||
errorMessage.includes('Fix external API key')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,22 +58,22 @@ export function isAuthenticationError(errorMessage: string): boolean {
|
||||
* @returns Classified error information
|
||||
*/
|
||||
export function classifyError(error: unknown): ErrorInfo {
|
||||
const message = error instanceof Error ? error.message : String(error || "Unknown error");
|
||||
const message = error instanceof Error ? error.message : String(error || 'Unknown error');
|
||||
const isAbort = isAbortError(error);
|
||||
const isAuth = isAuthenticationError(message);
|
||||
const isCancellation = isCancellationError(message);
|
||||
|
||||
let type: ErrorType;
|
||||
if (isAuth) {
|
||||
type = "authentication";
|
||||
type = 'authentication';
|
||||
} else if (isAbort) {
|
||||
type = "abort";
|
||||
type = 'abort';
|
||||
} else if (isCancellation) {
|
||||
type = "cancellation";
|
||||
type = 'cancellation';
|
||||
} else if (error instanceof Error) {
|
||||
type = "execution";
|
||||
type = 'execution';
|
||||
} else {
|
||||
type = "unknown";
|
||||
type = 'unknown';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -99,11 +96,11 @@ export function getUserFriendlyErrorMessage(error: unknown): string {
|
||||
const info = classifyError(error);
|
||||
|
||||
if (info.isAbort) {
|
||||
return "Operation was cancelled";
|
||||
return 'Operation was cancelled';
|
||||
}
|
||||
|
||||
if (info.isAuth) {
|
||||
return "Authentication failed. Please check your API key.";
|
||||
return 'Authentication failed. Please check your API key.';
|
||||
}
|
||||
|
||||
return info.message;
|
||||
@@ -130,5 +127,5 @@ export function getUserFriendlyErrorMessage(error: unknown): string {
|
||||
* ```
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : "Unknown error";
|
||||
return error instanceof Error ? error.message : 'Unknown error';
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* File system utilities that handle symlinks safely
|
||||
*/
|
||||
|
||||
import { secureFs } from "@automaker/platform";
|
||||
import path from "path";
|
||||
import { secureFs } from '@automaker/platform';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Create a directory, handling symlinks safely to avoid ELOOP errors.
|
||||
@@ -23,10 +23,10 @@ export async function mkdirSafe(dirPath: string): Promise<void> {
|
||||
throw new Error(`Path exists and is not a directory: ${resolvedPath}`);
|
||||
} catch (error: any) {
|
||||
// ENOENT means path doesn't exist - we should create it
|
||||
if (error.code !== "ENOENT") {
|
||||
if (error.code !== 'ENOENT') {
|
||||
// Some other error (could be ELOOP in parent path)
|
||||
// If it's ELOOP, the path involves symlinks - don't try to create
|
||||
if (error.code === "ELOOP") {
|
||||
if (error.code === 'ELOOP') {
|
||||
console.warn(`[fs-utils] Symlink loop detected at ${resolvedPath}, skipping mkdir`);
|
||||
return;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export async function mkdirSafe(dirPath: string): Promise<void> {
|
||||
await secureFs.mkdir(resolvedPath, { recursive: true });
|
||||
} catch (error: any) {
|
||||
// Handle race conditions and symlink issues
|
||||
if (error.code === "EEXIST" || error.code === "ELOOP") {
|
||||
if (error.code === 'EEXIST' || error.code === 'ELOOP') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
@@ -55,11 +55,11 @@ export async function existsSafe(filePath: string): Promise<boolean> {
|
||||
await secureFs.lstat(filePath);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
// ELOOP or other errors - path exists but is problematic
|
||||
if (error.code === "ELOOP") {
|
||||
if (error.code === 'ELOOP') {
|
||||
return true; // Symlink exists, even if looping
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
* - Path resolution (relative/absolute)
|
||||
*/
|
||||
|
||||
import { secureFs } from "@automaker/platform";
|
||||
import path from "path";
|
||||
import { secureFs } from '@automaker/platform';
|
||||
import path from 'path';
|
||||
import type { ImageData, ImageContentBlock } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* MIME type mapping for image file extensions
|
||||
*/
|
||||
const IMAGE_MIME_TYPES: Record<string, string> = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ const IMAGE_MIME_TYPES: Record<string, string> = {
|
||||
*/
|
||||
export function getMimeTypeForImage(imagePath: string): string {
|
||||
const ext = path.extname(imagePath).toLowerCase();
|
||||
return IMAGE_MIME_TYPES[ext] || "image/png";
|
||||
return IMAGE_MIME_TYPES[ext] || 'image/png';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,8 +42,8 @@ export function getMimeTypeForImage(imagePath: string): string {
|
||||
* @throws Error if file cannot be read
|
||||
*/
|
||||
export async function readImageAsBase64(imagePath: string): Promise<ImageData> {
|
||||
const imageBuffer = await secureFs.readFile(imagePath) as Buffer;
|
||||
const base64Data = imageBuffer.toString("base64");
|
||||
const imageBuffer = (await secureFs.readFile(imagePath)) as Buffer;
|
||||
const base64Data = imageBuffer.toString('base64');
|
||||
const mimeType = getMimeTypeForImage(imagePath);
|
||||
|
||||
return {
|
||||
@@ -71,16 +71,15 @@ export async function convertImagesToContentBlocks(
|
||||
for (const imagePath of imagePaths) {
|
||||
try {
|
||||
// Resolve to absolute path if needed
|
||||
const absolutePath = workDir && !path.isAbsolute(imagePath)
|
||||
? path.join(workDir, imagePath)
|
||||
: imagePath;
|
||||
const absolutePath =
|
||||
workDir && !path.isAbsolute(imagePath) ? path.join(workDir, imagePath) : imagePath;
|
||||
|
||||
const imageData = await readImageAsBase64(absolutePath);
|
||||
|
||||
blocks.push({
|
||||
type: "image",
|
||||
type: 'image',
|
||||
source: {
|
||||
type: "base64",
|
||||
type: 'base64',
|
||||
media_type: imageData.mimeType,
|
||||
data: imageData.base64,
|
||||
},
|
||||
@@ -103,10 +102,10 @@ export async function convertImagesToContentBlocks(
|
||||
*/
|
||||
export function formatImagePathsForPrompt(imagePaths: string[]): string {
|
||||
if (imagePaths.length === 0) {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
let text = "\n\nAttached images:\n";
|
||||
let text = '\n\nAttached images:\n';
|
||||
for (const imagePath of imagePaths) {
|
||||
text += `- ${imagePath}\n`;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function createLogger(context: string) {
|
||||
|
||||
debug: (...args: unknown[]): void => {
|
||||
if (currentLogLevel >= LogLevel.DEBUG) {
|
||||
console.log(prefix, "[DEBUG]", ...args);
|
||||
console.log(prefix, '[DEBUG]', ...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* ```
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
return p.replace(/\\/g, "/");
|
||||
return p.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,10 +45,7 @@ export function normalizePath(p: string): string {
|
||||
* pathsEqual(null, null); // true
|
||||
* ```
|
||||
*/
|
||||
export function pathsEqual(
|
||||
p1: string | undefined | null,
|
||||
p2: string | undefined | null
|
||||
): boolean {
|
||||
export function pathsEqual(p1: string | undefined | null, p2: string | undefined | null): boolean {
|
||||
if (!p1 || !p2) return p1 === p2;
|
||||
return normalizePath(p1) === normalizePath(p2);
|
||||
}
|
||||
|
||||
@@ -8,16 +8,18 @@
|
||||
* - Supports both vision and non-vision models
|
||||
*/
|
||||
|
||||
import { convertImagesToContentBlocks, formatImagePathsForPrompt } from "./image-handler.js";
|
||||
import { convertImagesToContentBlocks, formatImagePathsForPrompt } from './image-handler.js';
|
||||
|
||||
/**
|
||||
* Content that can be either simple text or structured blocks
|
||||
*/
|
||||
export type PromptContent = string | Array<{
|
||||
type: string;
|
||||
text?: string;
|
||||
source?: object;
|
||||
}>;
|
||||
export type PromptContent =
|
||||
| string
|
||||
| Array<{
|
||||
type: string;
|
||||
text?: string;
|
||||
source?: object;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Result of building a prompt with optional images
|
||||
@@ -62,7 +64,7 @@ export async function buildPromptWithImages(
|
||||
|
||||
// Add text block if we have text
|
||||
if (textContent.trim()) {
|
||||
contentBlocks.push({ type: "text", text: textContent });
|
||||
contentBlocks.push({ type: 'text', text: textContent });
|
||||
}
|
||||
|
||||
// Add image blocks
|
||||
@@ -71,9 +73,7 @@ export async function buildPromptWithImages(
|
||||
|
||||
// Return appropriate format
|
||||
const content: PromptContent =
|
||||
contentBlocks.length > 1 || contentBlocks[0]?.type === "image"
|
||||
? contentBlocks
|
||||
: textContent;
|
||||
contentBlocks.length > 1 || contentBlocks[0]?.type === 'image' ? contentBlocks : textContent;
|
||||
|
||||
return { content, hasImages: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user