Files
automaker/apps/server/src/routes/setup/routes/cursor-config.ts
Shirone 88aba360e3 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>
2026-01-03 03:35:33 +01:00

412 lines
12 KiB
TypeScript

/**
* Cursor CLI configuration routes
*
* Provides endpoints for managing Cursor CLI configuration:
* - GET /api/setup/cursor-config - Get current configuration
* - POST /api/setup/cursor-config/default-model - Set default model
* - POST /api/setup/cursor-config/models - Set enabled models
*
* Cursor CLI Permissions endpoints:
* - GET /api/setup/cursor-permissions - Get permissions config
* - POST /api/setup/cursor-permissions/profile - Apply a permission profile
* - POST /api/setup/cursor-permissions/custom - Set custom permissions
* - DELETE /api/setup/cursor-permissions - Delete project permissions (use global)
*/
import type { Request, Response } from 'express';
import path from 'path';
import { CursorConfigManager } from '../../../providers/cursor-config-manager.js';
import {
CURSOR_MODEL_MAP,
CURSOR_PERMISSION_PROFILES,
type CursorModelId,
type CursorPermissionProfile,
type CursorCliPermissions,
} from '@automaker/types';
import {
readGlobalConfig,
readProjectConfig,
getEffectivePermissions,
applyProfileToProject,
applyProfileGlobally,
writeProjectConfig,
deleteProjectConfig,
detectProfile,
hasProjectConfig,
getAvailableProfiles,
generateExampleConfig,
} 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
*/
export function createGetCursorConfigHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const projectPath = req.query.projectPath as string;
if (!projectPath) {
res.status(400).json({
success: false,
error: 'projectPath query parameter is required',
});
return;
}
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
const configManager = new CursorConfigManager(projectPath);
res.json({
success: true,
config: configManager.getConfig(),
availableModels: Object.values(CURSOR_MODEL_MAP),
});
} catch (error) {
logError(error, 'Get Cursor config failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}
/**
* Creates handler for POST /api/setup/cursor-config/default-model
* Sets the default Cursor model
*/
export function createSetCursorDefaultModelHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { model, projectPath } = req.body;
if (!projectPath) {
res.status(400).json({
success: false,
error: 'projectPath is required',
});
return;
}
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
if (!model || !(model in CURSOR_MODEL_MAP)) {
res.status(400).json({
success: false,
error: `Invalid model ID. Valid models: ${Object.keys(CURSOR_MODEL_MAP).join(', ')}`,
});
return;
}
const configManager = new CursorConfigManager(projectPath);
configManager.setDefaultModel(model as CursorModelId);
res.json({ success: true, model });
} catch (error) {
logError(error, 'Set Cursor default model failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}
/**
* Creates handler for POST /api/setup/cursor-config/models
* Sets the enabled Cursor models list
*/
export function createSetCursorModelsHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { models, projectPath } = req.body;
if (!projectPath) {
res.status(400).json({
success: false,
error: 'projectPath is required',
});
return;
}
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
if (!Array.isArray(models)) {
res.status(400).json({
success: false,
error: 'Models must be an array',
});
return;
}
// Filter to valid models only
const validModels = models.filter((m): m is CursorModelId => m in CURSOR_MODEL_MAP);
if (validModels.length === 0) {
res.status(400).json({
success: false,
error: 'No valid models provided',
});
return;
}
const configManager = new CursorConfigManager(projectPath);
configManager.setEnabledModels(validModels);
res.json({ success: true, models: validModels });
} catch (error) {
logError(error, 'Set Cursor models failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}
// =============================================================================
// Cursor CLI Permissions Handlers
// =============================================================================
/**
* Creates handler for GET /api/setup/cursor-permissions
* Returns current permissions configuration and available profiles
*/
export function createGetCursorPermissionsHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const projectPath = req.query.projectPath as string | undefined;
// Validate path if provided
if (projectPath) {
validateProjectPath(projectPath);
}
// Get global config
const globalConfig = await readGlobalConfig();
// Get project config if path provided
const projectConfig = projectPath ? await readProjectConfig(projectPath) : null;
// Get effective permissions
const effectivePermissions = await getEffectivePermissions(projectPath);
// Detect which profile is active
const activeProfile = detectProfile(effectivePermissions);
// Check if project has its own config
const hasProject = projectPath ? await hasProjectConfig(projectPath) : false;
res.json({
success: true,
globalPermissions: globalConfig?.permissions || null,
projectPermissions: projectConfig?.permissions || null,
effectivePermissions,
activeProfile,
hasProjectConfig: hasProject,
availableProfiles: getAvailableProfiles(),
});
} catch (error) {
logError(error, 'Get Cursor permissions failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}
/**
* Creates handler for POST /api/setup/cursor-permissions/profile
* Applies a predefined permission profile
*/
export function createApplyPermissionProfileHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { profileId, projectPath, scope } = req.body as {
profileId: CursorPermissionProfile;
projectPath?: string;
scope: 'global' | 'project';
};
// Validate profile
const validProfiles = CURSOR_PERMISSION_PROFILES.map((p) => p.id);
if (!validProfiles.includes(profileId)) {
res.status(400).json({
success: false,
error: `Invalid profile. Valid profiles: ${validProfiles.join(', ')}`,
});
return;
}
if (scope === 'project') {
if (!projectPath) {
res.status(400).json({
success: false,
error: 'projectPath is required for project scope',
});
return;
}
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
await applyProfileToProject(projectPath, profileId);
} else {
await applyProfileGlobally(profileId);
}
res.json({
success: true,
message: `Applied "${profileId}" profile to ${scope}`,
scope,
profileId,
});
} catch (error) {
logError(error, 'Apply Cursor permission profile failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}
/**
* Creates handler for POST /api/setup/cursor-permissions/custom
* Sets custom permissions for a project
*/
export function createSetCustomPermissionsHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, permissions } = req.body as {
projectPath: string;
permissions: CursorCliPermissions;
};
if (!projectPath) {
res.status(400).json({
success: false,
error: 'projectPath is required',
});
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,
error: 'permissions must have allow and deny arrays',
});
return;
}
await writeProjectConfig(projectPath, {
version: 1,
permissions,
});
res.json({
success: true,
message: 'Custom permissions saved',
permissions,
});
} catch (error) {
logError(error, 'Set custom Cursor permissions failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}
/**
* Creates handler for DELETE /api/setup/cursor-permissions
* Deletes project-level permissions (falls back to global)
*/
export function createDeleteProjectPermissionsHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const projectPath = req.query.projectPath as string;
if (!projectPath) {
res.status(400).json({
success: false,
error: 'projectPath query parameter is required',
});
return;
}
// Validate path to prevent traversal attacks
validateProjectPath(projectPath);
await deleteProjectConfig(projectPath);
res.json({
success: true,
message: 'Project permissions deleted, using global config',
});
} catch (error) {
logError(error, 'Delete Cursor project permissions failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}
/**
* Creates handler for GET /api/setup/cursor-permissions/example
* Returns an example config file for a profile
*/
export function createGetExampleConfigHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const profileId = (req.query.profileId as CursorPermissionProfile) || 'development';
const exampleConfig = generateExampleConfig(profileId);
res.json({
success: true,
profileId,
config: exampleConfig,
});
} catch (error) {
logError(error, 'Get example Cursor config failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}