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,30 +188,64 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
logger.debug(`[DescribeFile] Using model: ${model}`); logger.debug(`[DescribeFile] Using model: ${model}`);
// Use centralized SDK options with proper cwd validation let description: string;
// 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* () { // Route to appropriate provider based on model type
yield { if (isCursorModel(model)) {
type: 'user' as const, // Use Cursor provider for Cursor models
session_id: '', logger.info(`[DescribeFile] Using Cursor provider for model: ${model}`);
message: { role: 'user' as const, content: promptContent },
parent_tool_use_id: null,
};
})();
const stream = query({ prompt: promptGenerator, options: sdkOptions }); const provider = ProviderFactory.getProviderForModel(model);
// Extract the description from the response // Build a simple text prompt for Cursor (no multi-part content blocks)
const description = await extractTextFromStream(stream); 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) { 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,40 +348,79 @@ export function createDescribeImageHandler(
logger.info(`[${requestId}] Using model: ${model}`); logger.info(`[${requestId}] Using model: ${model}`);
// Use the same centralized option builder used across the server (validates cwd) let description: string;
const sdkOptions = createCustomOptions({
cwd,
model,
maxTurns: 1,
allowedTools: [],
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
});
logger.info( // Route to appropriate provider based on model type
`[${requestId}] SDK options model=${sdkOptions.model} maxTurns=${sdkOptions.maxTurns} allowedTools=${JSON.stringify( if (isCursorModel(model)) {
sdkOptions.allowedTools // Use Cursor provider for Cursor models
)} sandbox=${JSON.stringify(sdkOptions.sandbox)}` // Note: Cursor may have limited support for image content blocks
); logger.info(`[${requestId}] Using Cursor provider for model: ${model}`);
const promptGenerator = (async function* () { const provider = ProviderFactory.getProviderForModel(model);
yield {
type: 'user' as const,
session_id: '',
message: { role: 'user' as const, content: promptContent },
parent_tool_use_id: null,
};
})();
logger.info(`[${requestId}] Calling query()...`); // Build prompt with image reference for Cursor
const queryStart = Date.now(); // Note: Cursor CLI may not support base64 image blocks directly,
const stream = query({ prompt: promptGenerator, options: sdkOptions }); // so we include the image path as context
logger.info(`[${requestId}] query() returned stream in ${Date.now() - queryStart}ms`); const cursorPrompt = `${instructionText}\n\nImage file: ${actualPath}\nMIME type: ${imageData.mimeType}`;
// Extract the description from the response let responseText = '';
const extractStart = Date.now(); const queryStart = Date.now();
const description = await extractTextFromStream(stream, requestId); for await (const msg of provider.executeQuery({
logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`); 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) { 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);