feat(server): Add Cursor provider support for describe-file and describe-image routes

- describe-file.ts: Route to Cursor provider when using Cursor models (composer-1, etc.)
- describe-image.ts: Route to Cursor provider with image path context for Cursor models
- auto-mode-service.ts: Fix logging to use console.log instead of this.logger

Both routes now detect Cursor models using isCursorModel() and use
ProviderFactory.getProviderForModel() to get the appropriate provider
instead of always using the Claude SDK.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-30 14:59:25 +01:00
parent 68cefe43fb
commit 34e51ddc3d
3 changed files with 129 additions and 54 deletions

View File

@@ -13,10 +13,11 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils'; 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 { PathNotAllowedError } from '@automaker/platform';
import { resolveModelString } from '@automaker/model-resolver'; import { resolveModelString } from '@automaker/model-resolver';
import { createCustomOptions } from '../../../lib/sdk-options.js'; import { createCustomOptions } from '../../../lib/sdk-options.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import * as secureFs from '../../../lib/secure-fs.js'; import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path'; import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js'; import type { SettingsService } from '../../../services/settings-service.js';
@@ -187,6 +188,39 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
logger.debug(`[DescribeFile] Using model: ${model}`); logger.debug(`[DescribeFile] Using model: ${model}`);
let description: string;
// 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 provider = ProviderFactory.getProviderForModel(model);
// 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 // Use centralized SDK options with proper cwd validation
// No tools needed since we're passing file content directly // No tools needed since we're passing file content directly
const sdkOptions = createCustomOptions({ const sdkOptions = createCustomOptions({
@@ -210,7 +244,8 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
const stream = query({ prompt: promptGenerator, options: sdkOptions }); const stream = query({ prompt: promptGenerator, options: sdkOptions });
// Extract the description from the response // Extract the description from the response
const description = await extractTextFromStream(stream); description = await extractTextFromStream(stream);
}
if (!description || description.trim().length === 0) { if (!description || description.trim().length === 0) {
logger.warn('Received empty response from Claude'); logger.warn('Received empty response from Claude');

View File

@@ -14,9 +14,10 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger, readImageAsBase64 } from '@automaker/utils'; 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 { resolveModelString } from '@automaker/model-resolver';
import { createCustomOptions } from '../../../lib/sdk-options.js'; import { createCustomOptions } from '../../../lib/sdk-options.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js'; import type { SettingsService } from '../../../services/settings-service.js';
@@ -347,6 +348,44 @@ export function createDescribeImageHandler(
logger.info(`[${requestId}] Using model: ${model}`); logger.info(`[${requestId}] Using model: ${model}`);
let description: string;
// 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 provider = ProviderFactory.getProviderForModel(model);
// 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}`;
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) // Use the same centralized option builder used across the server (validates cwd)
const sdkOptions = createCustomOptions({ const sdkOptions = createCustomOptions({
cwd, cwd,
@@ -379,8 +418,9 @@ export function createDescribeImageHandler(
// Extract the description from the response // Extract the description from the response
const extractStart = Date.now(); const extractStart = Date.now();
const description = await extractTextFromStream(stream, requestId); description = await extractTextFromStream(stream, requestId);
logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`); logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`);
}
if (!description || description.trim().length === 0) { if (!description || description.trim().length === 0) {
logger.warn(`[${requestId}] Received empty response from Claude`); logger.warn(`[${requestId}] Received empty response from Claude`);

View File

@@ -1146,7 +1146,7 @@ Format your response as a structured markdown document.`;
const projectAnalysisModel = const projectAnalysisModel =
settings?.phaseModels?.projectAnalysisModel || DEFAULT_PHASE_MODELS.projectAnalysisModel; settings?.phaseModels?.projectAnalysisModel || DEFAULT_PHASE_MODELS.projectAnalysisModel;
const analysisModel = resolveModelString(projectAnalysisModel, DEFAULT_MODELS.claude); 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); const provider = ProviderFactory.getProviderForModel(analysisModel);