diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index 5d940f67..426cf73d 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -71,7 +71,7 @@ export function validateWorkingDirectory(cwd: string): void { * - iCloud Drive: ~/Library/Mobile Documents/ * - Box: ~/Library/CloudStorage/Box-* * - * @see https://github.com/anthropics/claude-code/issues/XXX (TODO: file upstream issue) + * Note: This is a known limitation when using cloud storage paths. */ /** diff --git a/apps/server/src/providers/cursor-provider.ts b/apps/server/src/providers/cursor-provider.ts index 56b82e56..ca708874 100644 --- a/apps/server/src/providers/cursor-provider.ts +++ b/apps/server/src/providers/cursor-provider.ts @@ -614,6 +614,16 @@ export class CursorProvider extends CliProvider { ); } + // MCP servers are not yet supported by Cursor CLI - log warning but continue + if (options.mcpServers && Object.keys(options.mcpServers).length > 0) { + const serverCount = Object.keys(options.mcpServers).length; + logger.warn( + `MCP servers configured (${serverCount}) but not yet supported by Cursor CLI in AutoMaker. ` + + `MCP support for Cursor will be added in a future release. ` + + `The configured MCP servers will be ignored for this execution.` + ); + } + // Extract prompt text to pass via stdin (avoids shell escaping issues) const promptText = this.extractPromptText(options); @@ -643,7 +653,8 @@ export class CursorProvider extends CliProvider { // Log raw event for debugging if (debugRawEvents) { - logger.info(`[RAW EVENT] type=${event.type} subtype=${(event as any).subtype || 'none'}`); + const subtype = 'subtype' in event ? (event.subtype as string) : 'none'; + logger.info(`[RAW EVENT] type=${event.type} subtype=${subtype}`); if (event.type === 'tool_call') { const toolEvent = event as CursorToolCallEvent; const tc = toolEvent.tool_call; @@ -949,6 +960,14 @@ export class CursorProvider extends CliProvider { }; } + /** + * Get the detected CLI path (public accessor for status endpoints) + */ + getCliPath(): string | null { + this.ensureCliDetected(); + return this.cliPath; + } + /** * Get available Cursor models */ @@ -960,7 +979,7 @@ export class CursorProvider extends CliProvider { provider: 'cursor', description: config.description, supportsTools: true, - supportsVision: false, + supportsVision: config.supportsVision, })); } diff --git a/apps/server/src/providers/types.ts b/apps/server/src/providers/types.ts index 32b2dff3..b995d0fb 100644 --- a/apps/server/src/providers/types.ts +++ b/apps/server/src/providers/types.ts @@ -2,6 +2,7 @@ * Shared types for AI model providers * * Re-exports types from @automaker/types for consistency across the codebase. + * All provider types are defined in @automaker/types to avoid duplication. */ // Re-export all provider types from @automaker/types @@ -13,80 +14,9 @@ export type { McpStdioServerConfig, McpSSEServerConfig, McpHttpServerConfig, + ContentBlock, + ProviderMessage, + InstallationStatus, + ValidationResult, + ModelDefinition, } from '@automaker/types'; - -/** - * Content block in a provider message (matches Claude SDK format) - */ -export interface ContentBlock { - type: 'text' | 'tool_use' | 'thinking' | 'tool_result'; - text?: string; - thinking?: string; - name?: string; - input?: unknown; - tool_use_id?: string; - content?: string; -} - -/** - * Message returned by a provider (matches Claude SDK streaming format) - */ -export interface ProviderMessage { - type: 'assistant' | 'user' | 'error' | 'result'; - subtype?: 'success' | 'error'; - session_id?: string; - message?: { - role: 'user' | 'assistant'; - content: ContentBlock[]; - }; - result?: string; - error?: string; - parent_tool_use_id?: string | null; -} - -/** - * Installation status for a provider - */ -export interface InstallationStatus { - installed: boolean; - path?: string; - version?: string; - /** - * How the provider was installed/detected - * - cli: Direct CLI binary - * - wsl: CLI accessed via Windows Subsystem for Linux - * - npm: Installed via npm - * - brew: Installed via Homebrew - * - sdk: Using SDK library - */ - method?: 'cli' | 'wsl' | 'npm' | 'brew' | 'sdk'; - hasApiKey?: boolean; - authenticated?: boolean; - error?: string; -} - -/** - * Validation result - */ -export interface ValidationResult { - valid: boolean; - errors: string[]; - warnings?: string[]; -} - -/** - * Model definition - */ -export interface ModelDefinition { - id: string; - name: string; - modelString: string; - provider: string; - description: string; - contextWindow?: number; - maxOutputTokens?: number; - supportsVision?: boolean; - supportsTools?: boolean; - tier?: 'basic' | 'standard' | 'premium'; - default?: boolean; -} diff --git a/apps/server/src/routes/setup/routes/cursor-config.ts b/apps/server/src/routes/setup/routes/cursor-config.ts index 3c410f6e..8b9c05ce 100644 --- a/apps/server/src/routes/setup/routes/cursor-config.ts +++ b/apps/server/src/routes/setup/routes/cursor-config.ts @@ -14,6 +14,7 @@ */ import type { Request, Response } from 'express'; +import path from 'path'; import { CursorConfigManager } from '../../../providers/cursor-config-manager.js'; import { CURSOR_MODEL_MAP, @@ -37,6 +38,27 @@ import { } from '../../../services/cursor-config-service.js'; import { getErrorMessage, logError } from '../common.js'; +/** + * Validate that a project path is safe (no path traversal) + * @throws Error if path contains traversal sequences + */ +function validateProjectPath(projectPath: string): void { + // Resolve to absolute path and check for traversal + const resolved = path.resolve(projectPath); + const normalized = path.normalize(projectPath); + + // Check for obvious traversal attempts + if (normalized.includes('..') || projectPath.includes('..')) { + throw new Error('Invalid project path: path traversal not allowed'); + } + + // Ensure the resolved path doesn't escape intended boundaries + // by checking if it starts with the normalized path components + if (!resolved.startsWith(path.resolve(normalized))) { + throw new Error('Invalid project path: path traversal detected'); + } +} + /** * Creates handler for GET /api/setup/cursor-config * Returns current Cursor configuration and available models @@ -54,6 +76,9 @@ export function createGetCursorConfigHandler() { return; } + // Validate path to prevent traversal attacks + validateProjectPath(projectPath); + const configManager = new CursorConfigManager(projectPath); res.json({ @@ -88,6 +113,9 @@ export function createSetCursorDefaultModelHandler() { return; } + // Validate path to prevent traversal attacks + validateProjectPath(projectPath); + if (!model || !(model in CURSOR_MODEL_MAP)) { res.status(400).json({ success: false, @@ -127,6 +155,9 @@ export function createSetCursorModelsHandler() { return; } + // Validate path to prevent traversal attacks + validateProjectPath(projectPath); + if (!Array.isArray(models)) { res.status(400).json({ success: false, @@ -173,6 +204,11 @@ export function createGetCursorPermissionsHandler() { try { const projectPath = req.query.projectPath as string | undefined; + // Validate path if provided + if (projectPath) { + validateProjectPath(projectPath); + } + // Get global config const globalConfig = await readGlobalConfig(); @@ -238,6 +274,8 @@ export function createApplyPermissionProfileHandler() { }); return; } + // Validate path to prevent traversal attacks + validateProjectPath(projectPath); await applyProfileToProject(projectPath, profileId); } else { await applyProfileGlobally(profileId); @@ -279,6 +317,9 @@ export function createSetCustomPermissionsHandler() { return; } + // Validate path to prevent traversal attacks + validateProjectPath(projectPath); + if (!permissions || !Array.isArray(permissions.allow) || !Array.isArray(permissions.deny)) { res.status(400).json({ success: false, @@ -324,6 +365,9 @@ export function createDeleteProjectPermissionsHandler() { return; } + // Validate path to prevent traversal attacks + validateProjectPath(projectPath); + await deleteProjectConfig(projectPath); res.json({ diff --git a/apps/server/src/routes/setup/routes/cursor-status.ts b/apps/server/src/routes/setup/routes/cursor-status.ts index afc99e6c..10cc50d5 100644 --- a/apps/server/src/routes/setup/routes/cursor-status.ts +++ b/apps/server/src/routes/setup/routes/cursor-status.ts @@ -24,10 +24,8 @@ export function createCursorStatusHandler() { provider.checkAuth(), ]); - // Get CLI path from provider (using type assertion since cliPath is private) - const cliPath = installed - ? (provider as unknown as { cliPath: string | null }).cliPath - : null; + // Get CLI path from provider using public accessor + const cliPath = installed ? provider.getCliPath() : null; res.json({ success: true, diff --git a/docs/add-new-cursor-model.md b/docs/add-new-cursor-model.md index f12828bf..928c27ec 100644 --- a/docs/add-new-cursor-model.md +++ b/docs/add-new-cursor-model.md @@ -46,7 +46,7 @@ export const CURSOR_MODEL_MAP: Record = { label: 'Your New Model', // Display name in UI description: 'Description of the model capabilities', hasThinking: false, // true if model has built-in reasoning - tier: 'pro', // 'free' or 'pro' + supportsVision: false, // true if model supports image inputs (currently all false) }, }; ``` @@ -72,13 +72,13 @@ The new model will automatically appear in: ## Model Config Fields -| Field | Type | Description | -| ------------- | ----------------- | -------------------------------------------------- | -| `id` | `string` | Must match the key in the map and the CLI model ID | -| `label` | `string` | Human-readable name shown in UI | -| `description` | `string` | Tooltip/help text explaining the model | -| `hasThinking` | `boolean` | Set `true` if model has built-in extended thinking | -| `tier` | `'free' \| 'pro'` | Subscription tier required to use this model | +| Field | Type | Description | +| ---------------- | --------- | --------------------------------------------------------------- | +| `id` | `string` | Must match the key in the map and the CLI model ID | +| `label` | `string` | Human-readable name shown in UI | +| `description` | `string` | Tooltip/help text explaining the model | +| `hasThinking` | `boolean` | Set `true` if model has built-in extended thinking | +| `supportsVision` | `boolean` | Set `true` if model supports image inputs (all false currently) | --- @@ -126,7 +126,7 @@ export const CURSOR_MODEL_MAP: Record = { label: 'Cursor Turbo', description: 'Optimized for speed with good quality balance', hasThinking: false, - tier: 'pro', + supportsVision: false, }, }; ``` @@ -151,4 +151,4 @@ After rebuilding, "Cursor Turbo" will appear in all model selection UIs. - The model ID must exactly match what Cursor CLI expects - Check Cursor's documentation for available models: https://cursor.com/docs - Models with `hasThinking: true` display a "Thinking" badge in the UI -- The `tier` field is informational and shown as a badge in selection UI +- Currently all models have `supportsVision: false` as Cursor CLI doesn't pass images to models diff --git a/libs/types/src/cursor-models.ts b/libs/types/src/cursor-models.ts index 4deb8b52..d9c67219 100644 --- a/libs/types/src/cursor-models.ts +++ b/libs/types/src/cursor-models.ts @@ -30,6 +30,8 @@ export interface CursorModelConfig { label: string; description: string; hasThinking: boolean; + /** Whether the model supports vision/image inputs (currently not supported by Cursor CLI) */ + supportsVision: boolean; } /** @@ -41,108 +43,126 @@ export const CURSOR_MODEL_MAP: Record = { label: 'Auto (Recommended)', description: 'Automatically selects the best model for each task', hasThinking: false, + supportsVision: false, // Vision not yet supported by Cursor CLI }, 'composer-1': { id: 'composer-1', label: 'Composer 1', description: 'Cursor Composer agent model optimized for multi-file edits', hasThinking: false, + supportsVision: false, }, 'sonnet-4.5': { id: 'sonnet-4.5', label: 'Claude Sonnet 4.5', description: 'Anthropic Claude Sonnet 4.5 via Cursor', hasThinking: false, + supportsVision: false, // Model supports vision but Cursor CLI doesn't pass images }, 'sonnet-4.5-thinking': { id: 'sonnet-4.5-thinking', label: 'Claude Sonnet 4.5 (Thinking)', description: 'Claude Sonnet 4.5 with extended thinking enabled', hasThinking: true, + supportsVision: false, }, 'opus-4.5': { id: 'opus-4.5', label: 'Claude Opus 4.5', description: 'Anthropic Claude Opus 4.5 via Cursor', hasThinking: false, + supportsVision: false, }, 'opus-4.5-thinking': { id: 'opus-4.5-thinking', label: 'Claude Opus 4.5 (Thinking)', description: 'Claude Opus 4.5 with extended thinking enabled', hasThinking: true, + supportsVision: false, }, 'opus-4.1': { id: 'opus-4.1', label: 'Claude Opus 4.1', description: 'Anthropic Claude Opus 4.1 via Cursor', hasThinking: false, + supportsVision: false, }, 'gemini-3-pro': { id: 'gemini-3-pro', label: 'Gemini 3 Pro', description: 'Google Gemini 3 Pro via Cursor', hasThinking: false, + supportsVision: false, }, 'gemini-3-flash': { id: 'gemini-3-flash', label: 'Gemini 3 Flash', description: 'Google Gemini 3 Flash (faster)', hasThinking: false, + supportsVision: false, }, 'gpt-5.2': { id: 'gpt-5.2', label: 'GPT-5.2', description: 'OpenAI GPT-5.2 via Cursor', hasThinking: false, + supportsVision: false, }, 'gpt-5.1': { id: 'gpt-5.1', label: 'GPT-5.1', description: 'OpenAI GPT-5.1 via Cursor', hasThinking: false, + supportsVision: false, }, 'gpt-5.2-high': { id: 'gpt-5.2-high', label: 'GPT-5.2 High', description: 'OpenAI GPT-5.2 with high compute', hasThinking: false, + supportsVision: false, }, 'gpt-5.1-high': { id: 'gpt-5.1-high', label: 'GPT-5.1 High', description: 'OpenAI GPT-5.1 with high compute', hasThinking: false, + supportsVision: false, }, 'gpt-5.1-codex': { id: 'gpt-5.1-codex', label: 'GPT-5.1 Codex', description: 'OpenAI GPT-5.1 Codex for code generation', hasThinking: false, + supportsVision: false, }, 'gpt-5.1-codex-high': { id: 'gpt-5.1-codex-high', label: 'GPT-5.1 Codex High', description: 'OpenAI GPT-5.1 Codex with high compute', hasThinking: false, + supportsVision: false, }, 'gpt-5.1-codex-max': { id: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max', description: 'OpenAI GPT-5.1 Codex Max capacity', hasThinking: false, + supportsVision: false, }, 'gpt-5.1-codex-max-high': { id: 'gpt-5.1-codex-max-high', label: 'GPT-5.1 Codex Max High', description: 'OpenAI GPT-5.1 Codex Max with high compute', hasThinking: false, + supportsVision: false, }, grok: { id: 'grok', label: 'Grok', description: 'xAI Grok via Cursor', hasThinking: false, + supportsVision: false, }, };