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/
* - 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)
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,
}));
}

View File

@@ -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;
}

View File

@@ -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({

View File

@@ -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,