diff --git a/apps/server/src/routes/context/routes/describe-file.ts b/apps/server/src/routes/context/routes/describe-file.ts index e6b58a83..3b30347c 100644 --- a/apps/server/src/routes/context/routes/describe-file.ts +++ b/apps/server/src/routes/context/routes/describe-file.ts @@ -13,10 +13,11 @@ import type { Request, Response } from 'express'; import { query } from '@anthropic-ai/claude-agent-sdk'; import { createLogger } from '@automaker/utils'; -import { DEFAULT_PHASE_MODELS } from '@automaker/types'; +import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types'; import { PathNotAllowedError } from '@automaker/platform'; import { resolveModelString } from '@automaker/model-resolver'; import { createCustomOptions } from '../../../lib/sdk-options.js'; +import { ProviderFactory } from '../../../providers/provider-factory.js'; import * as secureFs from '../../../lib/secure-fs.js'; import * as path from 'path'; import type { SettingsService } from '../../../services/settings-service.js'; @@ -187,30 +188,64 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`; logger.debug(`[DescribeFile] Using model: ${model}`); - // Use centralized SDK options with proper cwd validation - // No tools needed since we're passing file content directly - const sdkOptions = createCustomOptions({ - cwd, - model, - maxTurns: 1, - allowedTools: [], - autoLoadClaudeMd, - sandbox: { enabled: true, autoAllowBashIfSandboxed: true }, - }); + let description: string; - const promptGenerator = (async function* () { - yield { - type: 'user' as const, - session_id: '', - message: { role: 'user' as const, content: promptContent }, - parent_tool_use_id: null, - }; - })(); + // Route to appropriate provider based on model type + if (isCursorModel(model)) { + // Use Cursor provider for Cursor models + logger.info(`[DescribeFile] Using Cursor provider for model: ${model}`); - const stream = query({ prompt: promptGenerator, options: sdkOptions }); + const provider = ProviderFactory.getProviderForModel(model); - // Extract the description from the response - const description = await extractTextFromStream(stream); + // Build a simple text prompt for Cursor (no multi-part content blocks) + const cursorPrompt = `${instructionText}\n\n--- FILE CONTENT ---\n${contentToAnalyze}`; + + let responseText = ''; + for await (const msg of provider.executeQuery({ + prompt: cursorPrompt, + model, + cwd, + maxTurns: 1, + allowedTools: [], + })) { + if (msg.type === 'assistant' && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === 'text' && block.text) { + responseText += block.text; + } + } + } + } + description = responseText; + } else { + // Use Claude SDK for Claude models + logger.info(`[DescribeFile] Using Claude SDK for model: ${model}`); + + // Use centralized SDK options with proper cwd validation + // No tools needed since we're passing file content directly + const sdkOptions = createCustomOptions({ + cwd, + model, + maxTurns: 1, + allowedTools: [], + autoLoadClaudeMd, + sandbox: { enabled: true, autoAllowBashIfSandboxed: true }, + }); + + const promptGenerator = (async function* () { + yield { + type: 'user' as const, + session_id: '', + message: { role: 'user' as const, content: promptContent }, + parent_tool_use_id: null, + }; + })(); + + const stream = query({ prompt: promptGenerator, options: sdkOptions }); + + // Extract the description from the response + description = await extractTextFromStream(stream); + } if (!description || description.trim().length === 0) { logger.warn('Received empty response from Claude'); diff --git a/apps/server/src/routes/context/routes/describe-image.ts b/apps/server/src/routes/context/routes/describe-image.ts index 4424aeb9..3b87bd7b 100644 --- a/apps/server/src/routes/context/routes/describe-image.ts +++ b/apps/server/src/routes/context/routes/describe-image.ts @@ -14,9 +14,10 @@ import type { Request, Response } from 'express'; import { query } from '@anthropic-ai/claude-agent-sdk'; import { createLogger, readImageAsBase64 } from '@automaker/utils'; -import { DEFAULT_PHASE_MODELS } from '@automaker/types'; +import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types'; import { resolveModelString } from '@automaker/model-resolver'; import { createCustomOptions } from '../../../lib/sdk-options.js'; +import { ProviderFactory } from '../../../providers/provider-factory.js'; import * as fs from 'fs'; import * as path from 'path'; import type { SettingsService } from '../../../services/settings-service.js'; @@ -347,40 +348,79 @@ export function createDescribeImageHandler( logger.info(`[${requestId}] Using model: ${model}`); - // Use the same centralized option builder used across the server (validates cwd) - const sdkOptions = createCustomOptions({ - cwd, - model, - maxTurns: 1, - allowedTools: [], - autoLoadClaudeMd, - sandbox: { enabled: true, autoAllowBashIfSandboxed: true }, - }); + let description: string; - logger.info( - `[${requestId}] SDK options model=${sdkOptions.model} maxTurns=${sdkOptions.maxTurns} allowedTools=${JSON.stringify( - sdkOptions.allowedTools - )} sandbox=${JSON.stringify(sdkOptions.sandbox)}` - ); + // Route to appropriate provider based on model type + if (isCursorModel(model)) { + // Use Cursor provider for Cursor models + // Note: Cursor may have limited support for image content blocks + logger.info(`[${requestId}] Using Cursor provider for model: ${model}`); - const promptGenerator = (async function* () { - yield { - type: 'user' as const, - session_id: '', - message: { role: 'user' as const, content: promptContent }, - parent_tool_use_id: null, - }; - })(); + const provider = ProviderFactory.getProviderForModel(model); - logger.info(`[${requestId}] Calling query()...`); - const queryStart = Date.now(); - const stream = query({ prompt: promptGenerator, options: sdkOptions }); - logger.info(`[${requestId}] query() returned stream in ${Date.now() - queryStart}ms`); + // Build prompt with image reference for Cursor + // Note: Cursor CLI may not support base64 image blocks directly, + // so we include the image path as context + const cursorPrompt = `${instructionText}\n\nImage file: ${actualPath}\nMIME type: ${imageData.mimeType}`; - // Extract the description from the response - const extractStart = Date.now(); - const description = await extractTextFromStream(stream, requestId); - logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`); + let responseText = ''; + const queryStart = Date.now(); + for await (const msg of provider.executeQuery({ + prompt: cursorPrompt, + model, + cwd, + maxTurns: 1, + allowedTools: ['Read'], // Allow Read tool so Cursor can read the image if needed + })) { + if (msg.type === 'assistant' && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === 'text' && block.text) { + responseText += block.text; + } + } + } + } + logger.info(`[${requestId}] Cursor query completed in ${Date.now() - queryStart}ms`); + description = responseText; + } else { + // Use Claude SDK for Claude models (supports image content blocks) + logger.info(`[${requestId}] Using Claude SDK for model: ${model}`); + + // Use the same centralized option builder used across the server (validates cwd) + const sdkOptions = createCustomOptions({ + cwd, + model, + maxTurns: 1, + allowedTools: [], + autoLoadClaudeMd, + sandbox: { enabled: true, autoAllowBashIfSandboxed: true }, + }); + + logger.info( + `[${requestId}] SDK options model=${sdkOptions.model} maxTurns=${sdkOptions.maxTurns} allowedTools=${JSON.stringify( + sdkOptions.allowedTools + )} sandbox=${JSON.stringify(sdkOptions.sandbox)}` + ); + + const promptGenerator = (async function* () { + yield { + type: 'user' as const, + session_id: '', + message: { role: 'user' as const, content: promptContent }, + parent_tool_use_id: null, + }; + })(); + + logger.info(`[${requestId}] Calling query()...`); + const queryStart = Date.now(); + const stream = query({ prompt: promptGenerator, options: sdkOptions }); + logger.info(`[${requestId}] query() returned stream in ${Date.now() - queryStart}ms`); + + // Extract the description from the response + const extractStart = Date.now(); + description = await extractTextFromStream(stream, requestId); + logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`); + } if (!description || description.trim().length === 0) { logger.warn(`[${requestId}] Received empty response from Claude`); diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index f6281382..98231c36 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -1146,7 +1146,7 @@ Format your response as a structured markdown document.`; const projectAnalysisModel = settings?.phaseModels?.projectAnalysisModel || DEFAULT_PHASE_MODELS.projectAnalysisModel; const analysisModel = resolveModelString(projectAnalysisModel, DEFAULT_MODELS.claude); - this.logger.info('[AutoMode] Using model for project analysis:', analysisModel); + console.log('[AutoMode] Using model for project analysis:', analysisModel); const provider = ProviderFactory.getProviderForModel(analysisModel);