fix: improve Cursor CLI implementation with type safety and security fixes

- Add getCliPath() public method to CursorProvider to avoid private field access
- Add path validation to cursor-config routes to prevent traversal attacks
- Add supportsVision field to CursorModelConfig (all false - CLI limitation)
- Consolidate duplicate types in providers/types.ts (re-export from @automaker/types)
- Add MCP servers warning log instead of error (not yet supported by Cursor CLI)
- Fix debug log type safety (replace 'as any' with proper type narrowing)
- Update docs to remove non-existent tier field, add supportsVision field
- Remove outdated TODO comment in sdk-options.ts

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-03 03:35:33 +01:00
parent ec6d36bda5
commit 88aba360e3
7 changed files with 104 additions and 93 deletions

View File

@@ -71,7 +71,7 @@ export function validateWorkingDirectory(cwd: string): void {
* - iCloud Drive: ~/Library/Mobile Documents/ * - iCloud Drive: ~/Library/Mobile Documents/
* - Box: ~/Library/CloudStorage/Box-* * - 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.
*/ */
/** /**

View File

@@ -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) // Extract prompt text to pass via stdin (avoids shell escaping issues)
const promptText = this.extractPromptText(options); const promptText = this.extractPromptText(options);
@@ -643,7 +653,8 @@ export class CursorProvider extends CliProvider {
// Log raw event for debugging // Log raw event for debugging
if (debugRawEvents) { 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') { if (event.type === 'tool_call') {
const toolEvent = event as CursorToolCallEvent; const toolEvent = event as CursorToolCallEvent;
const tc = toolEvent.tool_call; 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 * Get available Cursor models
*/ */
@@ -960,7 +979,7 @@ export class CursorProvider extends CliProvider {
provider: 'cursor', provider: 'cursor',
description: config.description, description: config.description,
supportsTools: true, supportsTools: true,
supportsVision: false, supportsVision: config.supportsVision,
})); }));
} }

View File

@@ -2,6 +2,7 @@
* Shared types for AI model providers * Shared types for AI model providers
* *
* Re-exports types from @automaker/types for consistency across the codebase. * 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 // Re-export all provider types from @automaker/types
@@ -13,80 +14,9 @@ export type {
McpStdioServerConfig, McpStdioServerConfig,
McpSSEServerConfig, McpSSEServerConfig,
McpHttpServerConfig, McpHttpServerConfig,
ContentBlock,
ProviderMessage,
InstallationStatus,
ValidationResult,
ModelDefinition,
} from '@automaker/types'; } 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;
}

View File

@@ -14,6 +14,7 @@
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import path from 'path';
import { CursorConfigManager } from '../../../providers/cursor-config-manager.js'; import { CursorConfigManager } from '../../../providers/cursor-config-manager.js';
import { import {
CURSOR_MODEL_MAP, CURSOR_MODEL_MAP,
@@ -37,6 +38,27 @@ import {
} from '../../../services/cursor-config-service.js'; } from '../../../services/cursor-config-service.js';
import { getErrorMessage, logError } from '../common.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 * Creates handler for GET /api/setup/cursor-config
* Returns current Cursor configuration and available models * Returns current Cursor configuration and available models
@@ -54,6 +76,9 @@ export function createGetCursorConfigHandler() {
return; return;
} }
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
const configManager = new CursorConfigManager(projectPath); const configManager = new CursorConfigManager(projectPath);
res.json({ res.json({
@@ -88,6 +113,9 @@ export function createSetCursorDefaultModelHandler() {
return; return;
} }
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
if (!model || !(model in CURSOR_MODEL_MAP)) { if (!model || !(model in CURSOR_MODEL_MAP)) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
@@ -127,6 +155,9 @@ export function createSetCursorModelsHandler() {
return; return;
} }
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
if (!Array.isArray(models)) { if (!Array.isArray(models)) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
@@ -173,6 +204,11 @@ export function createGetCursorPermissionsHandler() {
try { try {
const projectPath = req.query.projectPath as string | undefined; const projectPath = req.query.projectPath as string | undefined;
// Validate path if provided
if (projectPath) {
validateProjectPath(projectPath);
}
// Get global config // Get global config
const globalConfig = await readGlobalConfig(); const globalConfig = await readGlobalConfig();
@@ -238,6 +274,8 @@ export function createApplyPermissionProfileHandler() {
}); });
return; return;
} }
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
await applyProfileToProject(projectPath, profileId); await applyProfileToProject(projectPath, profileId);
} else { } else {
await applyProfileGlobally(profileId); await applyProfileGlobally(profileId);
@@ -279,6 +317,9 @@ export function createSetCustomPermissionsHandler() {
return; return;
} }
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
if (!permissions || !Array.isArray(permissions.allow) || !Array.isArray(permissions.deny)) { if (!permissions || !Array.isArray(permissions.allow) || !Array.isArray(permissions.deny)) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
@@ -324,6 +365,9 @@ export function createDeleteProjectPermissionsHandler() {
return; return;
} }
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
await deleteProjectConfig(projectPath); await deleteProjectConfig(projectPath);
res.json({ res.json({

View File

@@ -24,10 +24,8 @@ export function createCursorStatusHandler() {
provider.checkAuth(), provider.checkAuth(),
]); ]);
// Get CLI path from provider (using type assertion since cliPath is private) // Get CLI path from provider using public accessor
const cliPath = installed const cliPath = installed ? provider.getCliPath() : null;
? (provider as unknown as { cliPath: string | null }).cliPath
: null;
res.json({ res.json({
success: true, success: true,

View File

@@ -46,7 +46,7 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
label: 'Your New Model', // Display name in UI label: 'Your New Model', // Display name in UI
description: 'Description of the model capabilities', description: 'Description of the model capabilities',
hasThinking: false, // true if model has built-in reasoning 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 ## Model Config Fields
| Field | Type | Description | | Field | Type | Description |
| ------------- | ----------------- | -------------------------------------------------- | | ---------------- | --------- | --------------------------------------------------------------- |
| `id` | `string` | Must match the key in the map and the CLI model ID | | `id` | `string` | Must match the key in the map and the CLI model ID |
| `label` | `string` | Human-readable name shown in UI | | `label` | `string` | Human-readable name shown in UI |
| `description` | `string` | Tooltip/help text explaining the model | | `description` | `string` | Tooltip/help text explaining the model |
| `hasThinking` | `boolean` | Set `true` if model has built-in extended thinking | | `hasThinking` | `boolean` | Set `true` if model has built-in extended thinking |
| `tier` | `'free' \| 'pro'` | Subscription tier required to use this model | | `supportsVision` | `boolean` | Set `true` if model supports image inputs (all false currently) |
--- ---
@@ -126,7 +126,7 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
label: 'Cursor Turbo', label: 'Cursor Turbo',
description: 'Optimized for speed with good quality balance', description: 'Optimized for speed with good quality balance',
hasThinking: false, 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 - The model ID must exactly match what Cursor CLI expects
- Check Cursor's documentation for available models: https://cursor.com/docs - Check Cursor's documentation for available models: https://cursor.com/docs
- Models with `hasThinking: true` display a "Thinking" badge in the UI - 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

View File

@@ -30,6 +30,8 @@ export interface CursorModelConfig {
label: string; label: string;
description: string; description: string;
hasThinking: boolean; 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<CursorModelId, CursorModelConfig> = {
label: 'Auto (Recommended)', label: 'Auto (Recommended)',
description: 'Automatically selects the best model for each task', description: 'Automatically selects the best model for each task',
hasThinking: false, hasThinking: false,
supportsVision: false, // Vision not yet supported by Cursor CLI
}, },
'composer-1': { 'composer-1': {
id: 'composer-1', id: 'composer-1',
label: 'Composer 1', label: 'Composer 1',
description: 'Cursor Composer agent model optimized for multi-file edits', description: 'Cursor Composer agent model optimized for multi-file edits',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'sonnet-4.5': { 'sonnet-4.5': {
id: 'sonnet-4.5', id: 'sonnet-4.5',
label: 'Claude Sonnet 4.5', label: 'Claude Sonnet 4.5',
description: 'Anthropic Claude Sonnet 4.5 via Cursor', description: 'Anthropic Claude Sonnet 4.5 via Cursor',
hasThinking: false, hasThinking: false,
supportsVision: false, // Model supports vision but Cursor CLI doesn't pass images
}, },
'sonnet-4.5-thinking': { 'sonnet-4.5-thinking': {
id: 'sonnet-4.5-thinking', id: 'sonnet-4.5-thinking',
label: 'Claude Sonnet 4.5 (Thinking)', label: 'Claude Sonnet 4.5 (Thinking)',
description: 'Claude Sonnet 4.5 with extended thinking enabled', description: 'Claude Sonnet 4.5 with extended thinking enabled',
hasThinking: true, hasThinking: true,
supportsVision: false,
}, },
'opus-4.5': { 'opus-4.5': {
id: 'opus-4.5', id: 'opus-4.5',
label: 'Claude Opus 4.5', label: 'Claude Opus 4.5',
description: 'Anthropic Claude Opus 4.5 via Cursor', description: 'Anthropic Claude Opus 4.5 via Cursor',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'opus-4.5-thinking': { 'opus-4.5-thinking': {
id: 'opus-4.5-thinking', id: 'opus-4.5-thinking',
label: 'Claude Opus 4.5 (Thinking)', label: 'Claude Opus 4.5 (Thinking)',
description: 'Claude Opus 4.5 with extended thinking enabled', description: 'Claude Opus 4.5 with extended thinking enabled',
hasThinking: true, hasThinking: true,
supportsVision: false,
}, },
'opus-4.1': { 'opus-4.1': {
id: 'opus-4.1', id: 'opus-4.1',
label: 'Claude Opus 4.1', label: 'Claude Opus 4.1',
description: 'Anthropic Claude Opus 4.1 via Cursor', description: 'Anthropic Claude Opus 4.1 via Cursor',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gemini-3-pro': { 'gemini-3-pro': {
id: 'gemini-3-pro', id: 'gemini-3-pro',
label: 'Gemini 3 Pro', label: 'Gemini 3 Pro',
description: 'Google Gemini 3 Pro via Cursor', description: 'Google Gemini 3 Pro via Cursor',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gemini-3-flash': { 'gemini-3-flash': {
id: 'gemini-3-flash', id: 'gemini-3-flash',
label: 'Gemini 3 Flash', label: 'Gemini 3 Flash',
description: 'Google Gemini 3 Flash (faster)', description: 'Google Gemini 3 Flash (faster)',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gpt-5.2': { 'gpt-5.2': {
id: 'gpt-5.2', id: 'gpt-5.2',
label: 'GPT-5.2', label: 'GPT-5.2',
description: 'OpenAI GPT-5.2 via Cursor', description: 'OpenAI GPT-5.2 via Cursor',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gpt-5.1': { 'gpt-5.1': {
id: 'gpt-5.1', id: 'gpt-5.1',
label: 'GPT-5.1', label: 'GPT-5.1',
description: 'OpenAI GPT-5.1 via Cursor', description: 'OpenAI GPT-5.1 via Cursor',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gpt-5.2-high': { 'gpt-5.2-high': {
id: 'gpt-5.2-high', id: 'gpt-5.2-high',
label: 'GPT-5.2 High', label: 'GPT-5.2 High',
description: 'OpenAI GPT-5.2 with high compute', description: 'OpenAI GPT-5.2 with high compute',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gpt-5.1-high': { 'gpt-5.1-high': {
id: 'gpt-5.1-high', id: 'gpt-5.1-high',
label: 'GPT-5.1 High', label: 'GPT-5.1 High',
description: 'OpenAI GPT-5.1 with high compute', description: 'OpenAI GPT-5.1 with high compute',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gpt-5.1-codex': { 'gpt-5.1-codex': {
id: 'gpt-5.1-codex', id: 'gpt-5.1-codex',
label: 'GPT-5.1 Codex', label: 'GPT-5.1 Codex',
description: 'OpenAI GPT-5.1 Codex for code generation', description: 'OpenAI GPT-5.1 Codex for code generation',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gpt-5.1-codex-high': { 'gpt-5.1-codex-high': {
id: 'gpt-5.1-codex-high', id: 'gpt-5.1-codex-high',
label: 'GPT-5.1 Codex High', label: 'GPT-5.1 Codex High',
description: 'OpenAI GPT-5.1 Codex with high compute', description: 'OpenAI GPT-5.1 Codex with high compute',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gpt-5.1-codex-max': { 'gpt-5.1-codex-max': {
id: 'gpt-5.1-codex-max', id: 'gpt-5.1-codex-max',
label: 'GPT-5.1 Codex Max', label: 'GPT-5.1 Codex Max',
description: 'OpenAI GPT-5.1 Codex Max capacity', description: 'OpenAI GPT-5.1 Codex Max capacity',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
'gpt-5.1-codex-max-high': { 'gpt-5.1-codex-max-high': {
id: 'gpt-5.1-codex-max-high', id: 'gpt-5.1-codex-max-high',
label: 'GPT-5.1 Codex Max High', label: 'GPT-5.1 Codex Max High',
description: 'OpenAI GPT-5.1 Codex Max with high compute', description: 'OpenAI GPT-5.1 Codex Max with high compute',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
grok: { grok: {
id: 'grok', id: 'grok',
label: 'Grok', label: 'Grok',
description: 'xAI Grok via Cursor', description: 'xAI Grok via Cursor',
hasThinking: false, hasThinking: false,
supportsVision: false,
}, },
}; };