mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -46,7 +46,7 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
|
||||
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<CursorModelId, CursorModelConfig> = {
|
||||
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
|
||||
|
||||
@@ -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<CursorModelId, CursorModelConfig> = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user