From dac916496c35fc4e07b64ca298cd6475ded48681 Mon Sep 17 00:00:00 2001 From: Kacper Date: Tue, 30 Dec 2025 17:08:18 +0100 Subject: [PATCH] feat(server): Implement Cursor CLI permissions management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added new routes and handlers for managing Cursor CLI permissions, including: - GET /api/setup/cursor-permissions: Retrieve current permissions configuration and available profiles. - POST /api/setup/cursor-permissions/profile: Apply a predefined permission profile (global or project). - POST /api/setup/cursor-permissions/custom: Set custom permissions for a project. - DELETE /api/setup/cursor-permissions: Delete project-level permissions, reverting to global settings. - GET /api/setup/cursor-permissions/example: Provide an example config file for a specified profile. Also introduced a new service for handling Cursor CLI configuration files and updated the UI to support permissions management. Affected files: - Added new routes in index.ts and cursor-config.ts - Created cursor-config-service.ts for permissions management logic - Updated UI components to display and manage permissions 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- apps/server/src/routes/setup/index.ts | 12 + .../src/routes/setup/routes/cursor-config.ts | 233 +++++++++++- .../src/services/cursor-config-service.ts | 280 +++++++++++++++ .../providers/cursor-settings-tab.tsx | 335 +++++++++++++++++- apps/ui/src/lib/http-api-client.ts | 67 ++++ libs/types/src/cursor-cli.ts | 199 +++++++++++ 6 files changed, 1121 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/services/cursor-config-service.ts diff --git a/apps/server/src/routes/setup/index.ts b/apps/server/src/routes/setup/index.ts index baa3c350..6c9f42a2 100644 --- a/apps/server/src/routes/setup/index.ts +++ b/apps/server/src/routes/setup/index.ts @@ -17,6 +17,11 @@ import { createGetCursorConfigHandler, createSetCursorDefaultModelHandler, createSetCursorModelsHandler, + createGetCursorPermissionsHandler, + createApplyPermissionProfileHandler, + createSetCustomPermissionsHandler, + createDeleteProjectPermissionsHandler, + createGetExampleConfigHandler, } from './routes/cursor-config.js'; export function createSetupRoutes(): Router { @@ -38,5 +43,12 @@ export function createSetupRoutes(): Router { router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler()); router.post('/cursor-config/models', createSetCursorModelsHandler()); + // Cursor CLI Permissions routes + router.get('/cursor-permissions', createGetCursorPermissionsHandler()); + router.post('/cursor-permissions/profile', createApplyPermissionProfileHandler()); + router.post('/cursor-permissions/custom', createSetCustomPermissionsHandler()); + router.delete('/cursor-permissions', createDeleteProjectPermissionsHandler()); + router.get('/cursor-permissions/example', createGetExampleConfigHandler()); + return router; } diff --git a/apps/server/src/routes/setup/routes/cursor-config.ts b/apps/server/src/routes/setup/routes/cursor-config.ts index a1d19f42..3c410f6e 100644 --- a/apps/server/src/routes/setup/routes/cursor-config.ts +++ b/apps/server/src/routes/setup/routes/cursor-config.ts @@ -5,11 +5,36 @@ * - 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 { CursorConfigManager } from '../../../providers/cursor-config-manager.js'; -import { CURSOR_MODEL_MAP, type CursorModelId } from '@automaker/types'; +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'; /** @@ -134,3 +159,209 @@ export function createSetCursorModelsHandler() { } }; } + +// ============================================================================= +// 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 => { + try { + const projectPath = req.query.projectPath as string | undefined; + + // 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 => { + 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; + } + 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 => { + try { + const { projectPath, permissions } = req.body as { + projectPath: string; + permissions: CursorCliPermissions; + }; + + if (!projectPath) { + res.status(400).json({ + success: false, + error: 'projectPath is required', + }); + return; + } + + 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 => { + try { + const projectPath = req.query.projectPath as string; + + if (!projectPath) { + res.status(400).json({ + success: false, + error: 'projectPath query parameter is required', + }); + return; + } + + 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 => { + 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), + }); + } + }; +} diff --git a/apps/server/src/services/cursor-config-service.ts b/apps/server/src/services/cursor-config-service.ts new file mode 100644 index 00000000..d84252b9 --- /dev/null +++ b/apps/server/src/services/cursor-config-service.ts @@ -0,0 +1,280 @@ +/** + * Cursor Config Service + * + * Manages Cursor CLI permissions configuration files: + * - Global: ~/.cursor/cli-config.json + * - Project: /.cursor/cli.json + * + * Based on: https://cursor.com/docs/cli/reference/configuration + */ + +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; +import { createLogger } from '@automaker/utils'; +import type { + CursorCliConfigFile, + CursorCliPermissions, + CursorPermissionProfile, +} from '@automaker/types'; +import { + CURSOR_STRICT_PROFILE, + CURSOR_DEVELOPMENT_PROFILE, + CURSOR_PERMISSION_PROFILES, +} from '@automaker/types'; + +const logger = createLogger('CursorConfigService'); + +/** + * Get the path to the global Cursor CLI config + */ +export function getGlobalConfigPath(): string { + // Windows: $env:USERPROFILE\.cursor\cli-config.json + // macOS/Linux: ~/.cursor/cli-config.json + // XDG_CONFIG_HOME override on Linux: $XDG_CONFIG_HOME/cursor/cli-config.json + const xdgConfig = process.env.XDG_CONFIG_HOME; + const cursorConfigDir = process.env.CURSOR_CONFIG_DIR; + + if (cursorConfigDir) { + return path.join(cursorConfigDir, 'cli-config.json'); + } + + if (process.platform === 'linux' && xdgConfig) { + return path.join(xdgConfig, 'cursor', 'cli-config.json'); + } + + return path.join(os.homedir(), '.cursor', 'cli-config.json'); +} + +/** + * Get the path to a project's Cursor CLI config + */ +export function getProjectConfigPath(projectPath: string): string { + return path.join(projectPath, '.cursor', 'cli.json'); +} + +/** + * Read the global Cursor CLI config + */ +export async function readGlobalConfig(): Promise { + const configPath = getGlobalConfigPath(); + + try { + const content = await fs.readFile(configPath, 'utf-8'); + const config = JSON.parse(content) as CursorCliConfigFile; + logger.debug('Read global Cursor config from:', configPath); + return config; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + logger.debug('Global Cursor config not found at:', configPath); + return null; + } + logger.error('Failed to read global Cursor config:', error); + throw error; + } +} + +/** + * Write the global Cursor CLI config + */ +export async function writeGlobalConfig(config: CursorCliConfigFile): Promise { + const configPath = getGlobalConfigPath(); + const configDir = path.dirname(configPath); + + // Ensure directory exists + await fs.mkdir(configDir, { recursive: true }); + + // Write config + await fs.writeFile(configPath, JSON.stringify(config, null, 2)); + logger.info('Wrote global Cursor config to:', configPath); +} + +/** + * Read a project's Cursor CLI config + */ +export async function readProjectConfig(projectPath: string): Promise { + const configPath = getProjectConfigPath(projectPath); + + try { + const content = await fs.readFile(configPath, 'utf-8'); + const config = JSON.parse(content) as CursorCliConfigFile; + logger.debug('Read project Cursor config from:', configPath); + return config; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + logger.debug('Project Cursor config not found at:', configPath); + return null; + } + logger.error('Failed to read project Cursor config:', error); + throw error; + } +} + +/** + * Write a project's Cursor CLI config + * + * Note: Project-level config ONLY supports permissions. + * The version field and other settings are global-only. + * See: https://cursor.com/docs/cli/reference/configuration + */ +export async function writeProjectConfig( + projectPath: string, + config: CursorCliConfigFile +): Promise { + const configPath = getProjectConfigPath(projectPath); + const configDir = path.dirname(configPath); + + // Ensure .cursor directory exists + await fs.mkdir(configDir, { recursive: true }); + + // Write config (project config ONLY supports permissions - no version field!) + const projectConfig = { + permissions: config.permissions, + }; + + await fs.writeFile(configPath, JSON.stringify(projectConfig, null, 2)); + logger.info('Wrote project Cursor config to:', configPath); +} + +/** + * Delete a project's Cursor CLI config + */ +export async function deleteProjectConfig(projectPath: string): Promise { + const configPath = getProjectConfigPath(projectPath); + + try { + await fs.unlink(configPath); + logger.info('Deleted project Cursor config:', configPath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } +} + +/** + * Get the effective permissions for a project + * Project config takes precedence over global config + */ +export async function getEffectivePermissions( + projectPath?: string +): Promise { + // Try project config first + if (projectPath) { + const projectConfig = await readProjectConfig(projectPath); + if (projectConfig?.permissions) { + return projectConfig.permissions; + } + } + + // Fall back to global config + const globalConfig = await readGlobalConfig(); + return globalConfig?.permissions || null; +} + +/** + * Apply a predefined permission profile to a project + */ +export async function applyProfileToProject( + projectPath: string, + profileId: CursorPermissionProfile +): Promise { + const profile = CURSOR_PERMISSION_PROFILES.find((p) => p.id === profileId); + + if (!profile) { + throw new Error(`Unknown permission profile: ${profileId}`); + } + + await writeProjectConfig(projectPath, { + version: 1, + permissions: profile.permissions, + }); + + logger.info(`Applied "${profile.name}" profile to project:`, projectPath); +} + +/** + * Apply a predefined permission profile globally + */ +export async function applyProfileGlobally(profileId: CursorPermissionProfile): Promise { + const profile = CURSOR_PERMISSION_PROFILES.find((p) => p.id === profileId); + + if (!profile) { + throw new Error(`Unknown permission profile: ${profileId}`); + } + + // Read existing global config to preserve other settings + const existingConfig = await readGlobalConfig(); + + await writeGlobalConfig({ + version: 1, + ...existingConfig, + permissions: profile.permissions, + }); + + logger.info(`Applied "${profile.name}" profile globally`); +} + +/** + * Detect which profile matches the current permissions + */ +export function detectProfile( + permissions: CursorCliPermissions | null +): CursorPermissionProfile | null { + if (!permissions) { + return null; + } + + // Check if permissions match a predefined profile + for (const profile of CURSOR_PERMISSION_PROFILES) { + const allowMatch = + JSON.stringify(profile.permissions.allow.sort()) === JSON.stringify(permissions.allow.sort()); + const denyMatch = + JSON.stringify(profile.permissions.deny.sort()) === JSON.stringify(permissions.deny.sort()); + + if (allowMatch && denyMatch) { + return profile.id; + } + } + + return 'custom'; +} + +/** + * Generate example config file content + */ +export function generateExampleConfig(profileId: CursorPermissionProfile = 'development'): string { + const profile = + CURSOR_PERMISSION_PROFILES.find((p) => p.id === profileId) || CURSOR_DEVELOPMENT_PROFILE; + + const config: CursorCliConfigFile = { + version: 1, + permissions: profile.permissions, + }; + + return JSON.stringify(config, null, 2); +} + +/** + * Check if a project has Cursor CLI config + */ +export async function hasProjectConfig(projectPath: string): Promise { + const configPath = getProjectConfigPath(projectPath); + + try { + await fs.access(configPath); + return true; + } catch { + return false; + } +} + +/** + * Get all available permission profiles + */ +export function getAvailableProfiles() { + return CURSOR_PERMISSION_PROFILES; +} + +// Export profile constants for convenience +export { CURSOR_STRICT_PROFILE, CURSOR_DEVELOPMENT_PROFILE }; diff --git a/apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx b/apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx index 46306d4c..f20d8bfc 100644 --- a/apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx +++ b/apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Select, @@ -9,13 +10,23 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Terminal, Info } from 'lucide-react'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { + Terminal, + Info, + Shield, + ShieldCheck, + ShieldAlert, + ChevronDown, + Copy, + Check, +} from 'lucide-react'; import { toast } from 'sonner'; import { getHttpApiClient } from '@/lib/http-api-client'; import { useAppStore } from '@/store/app-store'; import { useSetupStore } from '@/store/setup-store'; import { cn } from '@/lib/utils'; -import type { CursorModelId, CursorModelConfig } from '@automaker/types'; +import type { CursorModelId, CursorModelConfig, CursorPermissionProfile } from '@automaker/types'; import { CURSOR_MODEL_MAP } from '@automaker/types'; import { CursorCliStatus, @@ -30,16 +41,40 @@ interface CursorStatus { method?: string; } +interface PermissionsData { + activeProfile: CursorPermissionProfile | null; + effectivePermissions: { allow: string[]; deny: string[] } | null; + hasProjectConfig: boolean; + availableProfiles: Array<{ + id: string; + name: string; + description: string; + permissions: { allow: string[]; deny: string[] }; + }>; +} + export function CursorSettingsTab() { // Global settings from store - const { enabledCursorModels, cursorDefaultModel, setCursorDefaultModel, toggleCursorModel } = - useAppStore(); + const { + enabledCursorModels, + cursorDefaultModel, + setCursorDefaultModel, + toggleCursorModel, + currentProject, + } = useAppStore(); const { setCursorCliStatus } = useSetupStore(); const [status, setStatus] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + // Permissions state + const [permissions, setPermissions] = useState(null); + const [isLoadingPermissions, setIsLoadingPermissions] = useState(false); + const [isSavingPermissions, setIsSavingPermissions] = useState(false); + const [permissionsExpanded, setPermissionsExpanded] = useState(false); + const [copiedConfig, setCopiedConfig] = useState(false); + // All available models from the model map const availableModels: CursorModelConfig[] = Object.values(CURSOR_MODEL_MAP); @@ -105,6 +140,79 @@ export function CursorSettingsTab() { } }; + // Load permissions data + const loadPermissions = useCallback(async () => { + setIsLoadingPermissions(true); + try { + const api = getHttpApiClient(); + const result = await api.setup.getCursorPermissions(currentProject?.path); + + if (result.success) { + setPermissions({ + activeProfile: result.activeProfile || null, + effectivePermissions: result.effectivePermissions || null, + hasProjectConfig: result.hasProjectConfig || false, + availableProfiles: result.availableProfiles || [], + }); + } + } catch (error) { + console.error('Failed to load Cursor permissions:', error); + } finally { + setIsLoadingPermissions(false); + } + }, [currentProject?.path]); + + // Load permissions when tab is expanded + useEffect(() => { + if (permissionsExpanded && status?.installed && !permissions) { + loadPermissions(); + } + }, [permissionsExpanded, status?.installed, permissions, loadPermissions]); + + // Apply a permission profile + const handleApplyProfile = async ( + profileId: 'strict' | 'development', + scope: 'global' | 'project' + ) => { + setIsSavingPermissions(true); + try { + const api = getHttpApiClient(); + const result = await api.setup.applyCursorPermissionProfile( + profileId, + scope, + scope === 'project' ? currentProject?.path : undefined + ); + + if (result.success) { + toast.success(result.message || `Applied ${profileId} profile`); + await loadPermissions(); + } else { + toast.error(result.error || 'Failed to apply profile'); + } + } catch (error) { + toast.error('Failed to apply profile'); + } finally { + setIsSavingPermissions(false); + } + }; + + // Copy example config to clipboard + const handleCopyConfig = async (profileId: 'strict' | 'development') => { + try { + const api = getHttpApiClient(); + const result = await api.setup.getCursorExampleConfig(profileId); + + if (result.success && result.config) { + await navigator.clipboard.writeText(result.config); + setCopiedConfig(true); + toast.success('Config copied to clipboard'); + setTimeout(() => setCopiedConfig(false), 2000); + } + } catch (error) { + toast.error('Failed to copy config'); + } + }; + if (isLoading) { return (
@@ -238,6 +346,225 @@ export function CursorSettingsTab() {
)} + + {/* CLI Permissions Section */} + {status?.installed && ( + +
+ +
+
+
+ +
+
+

+ CLI Permissions +

+

+ Configure what Cursor CLI can do +

+
+
+
+ {permissions?.activeProfile && ( + + {permissions.activeProfile === 'strict' && ( + + )} + {permissions.activeProfile === 'development' && ( + + )} + {permissions.activeProfile} + + )} + +
+
+
+ + +
+ {/* Security Warning */} +
+ +
+ Security Notice +

+ Cursor CLI can execute shell commands based on its permission config. For + overnight automation, consider using the Strict profile to limit what commands + can run. +

+
+
+ + {isLoadingPermissions ? ( +
+
+
+ ) : ( + <> + {/* Permission Profiles */} +
+ +
+ {permissions?.availableProfiles.map((profile) => ( +
+
+
+
+ {profile.id === 'strict' ? ( + + ) : ( + + )} + {profile.name} + {permissions.activeProfile === profile.id && ( + + Active + + )} +
+

+ {profile.description} +

+
+ + {profile.permissions.allow.length} allowed + + | + + {profile.permissions.deny.length} denied + +
+
+
+ + {currentProject && ( + + )} +
+
+
+ ))} +
+
+ + {/* Config File Location */} +
+ +
+
+
+

Global Config

+

+ ~/.cursor/cli-config.json +

+
+ +
+
+

Project Config

+

+ <project>/.cursor/cli.json +

+ {permissions?.hasProjectConfig && ( + + Project override active + + )} +
+
+
+ + {/* Documentation Link */} +
+ Learn more about{' '} + + Cursor CLI permissions + +
+ + )} +
+ +
+ + )}
); } diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index dddf9eae..0c9bfd04 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -562,6 +562,73 @@ export class HttpApiClient implements ElectronAPI { error?: string; }> => this.post('/api/setup/cursor-config/models', { projectPath, models }), + // Cursor CLI Permissions + getCursorPermissions: ( + projectPath?: string + ): Promise<{ + success: boolean; + globalPermissions?: { allow: string[]; deny: string[] } | null; + projectPermissions?: { allow: string[]; deny: string[] } | null; + effectivePermissions?: { allow: string[]; deny: string[] } | null; + activeProfile?: 'strict' | 'development' | 'custom' | null; + hasProjectConfig?: boolean; + availableProfiles?: Array<{ + id: string; + name: string; + description: string; + permissions: { allow: string[]; deny: string[] }; + }>; + error?: string; + }> => + this.get( + `/api/setup/cursor-permissions${projectPath ? `?projectPath=${encodeURIComponent(projectPath)}` : ''}` + ), + + applyCursorPermissionProfile: ( + profileId: 'strict' | 'development', + scope: 'global' | 'project', + projectPath?: string + ): Promise<{ + success: boolean; + message?: string; + scope?: string; + profileId?: string; + error?: string; + }> => this.post('/api/setup/cursor-permissions/profile', { profileId, scope, projectPath }), + + setCursorCustomPermissions: ( + projectPath: string, + permissions: { allow: string[]; deny: string[] } + ): Promise<{ + success: boolean; + message?: string; + permissions?: { allow: string[]; deny: string[] }; + error?: string; + }> => this.post('/api/setup/cursor-permissions/custom', { projectPath, permissions }), + + deleteCursorProjectPermissions: ( + projectPath: string + ): Promise<{ + success: boolean; + message?: string; + error?: string; + }> => + this.httpDelete( + `/api/setup/cursor-permissions?projectPath=${encodeURIComponent(projectPath)}` + ), + + getCursorExampleConfig: ( + profileId?: 'strict' | 'development' + ): Promise<{ + success: boolean; + profileId?: string; + config?: string; + error?: string; + }> => + this.get( + `/api/setup/cursor-permissions/example${profileId ? `?profileId=${profileId}` : ''}` + ), + onInstallProgress: (callback: (progress: unknown) => void) => { return this.subscribeToEvent('agent:stream', callback); }, diff --git a/libs/types/src/cursor-cli.ts b/libs/types/src/cursor-cli.ts index a936c735..6929abd1 100644 --- a/libs/types/src/cursor-cli.ts +++ b/libs/types/src/cursor-cli.ts @@ -11,6 +11,205 @@ export interface CursorCliConfig { rules?: string[]; // .cursor/rules paths } +// ============================================================================= +// Cursor CLI Permissions Configuration +// Based on: https://cursor.com/docs/cli/reference/permissions +// ============================================================================= + +/** + * Permission string format for Cursor CLI + * Examples: + * - "Shell(git)" - Allow/deny git commands + * - "Shell(npm)" - Allow/deny npm commands + * - "Read(.env*)" - Allow/deny reading .env files + * - "Write(src/**)" - Allow/deny writing to src directory + */ +export type CursorPermissionString = string; + +/** + * Cursor CLI permissions configuration + * Used in ~/.cursor/cli-config.json or /.cursor/cli.json + */ +export interface CursorCliPermissions { + /** + * Permitted operations + * Format: "Shell(command)", "Read(path)", "Write(path)" + */ + allow: CursorPermissionString[]; + + /** + * Forbidden operations (takes precedence over allow) + * Format: "Shell(command)", "Read(path)", "Write(path)" + */ + deny: CursorPermissionString[]; +} + +/** + * Full Cursor CLI config file format (cli-config.json / cli.json) + * See: https://cursor.com/docs/cli/reference/configuration + */ +export interface CursorCliConfigFile { + /** Schema version (currently 1) */ + version: 1; + + /** Editor settings (global only) */ + editor?: { + vimMode?: boolean; + }; + + /** Model settings (global only) */ + model?: { + default?: string; + }; + + /** Permissions (can be project-level) */ + permissions?: CursorCliPermissions; +} + +/** + * Predefined permission profiles for different use cases + */ +export type CursorPermissionProfile = 'strict' | 'development' | 'custom'; + +/** + * Permission profile definitions + */ +export interface CursorPermissionProfileConfig { + id: CursorPermissionProfile; + name: string; + description: string; + permissions: CursorCliPermissions; +} + +/** + * Strict profile - For read-only operations + * Denies all shell commands and writes + */ +export const CURSOR_STRICT_PROFILE: CursorPermissionProfileConfig = { + id: 'strict', + name: 'Strict (Read-Only)', + description: 'Denies all shell commands and file writes. Safe for analysis tasks.', + permissions: { + allow: [ + 'Read(**/*)', // Allow reading all files + ], + deny: [ + 'Shell(*)', // Deny all shell commands + 'Write(**/*)', // Deny all file writes + 'Read(.env*)', // Deny reading env files + 'Read(**/*.pem)', // Deny reading private keys + 'Read(**/*.key)', // Deny reading key files + 'Read(**/credentials*)', // Deny reading credentials + ], + }, +}; + +/** + * Development profile - For feature implementation + * Allows safe operations, blocks destructive ones + */ +export const CURSOR_DEVELOPMENT_PROFILE: CursorPermissionProfileConfig = { + id: 'development', + name: 'Development', + description: 'Allows file edits and safe shell commands. Blocks destructive operations.', + permissions: { + allow: [ + 'Read(**/*)', // Allow reading all files + 'Write(**/*)', // Allow writing files + 'Shell(npm)', // npm install, run, test + 'Shell(pnpm)', // pnpm install, run, test + 'Shell(yarn)', // yarn install, run, test + 'Shell(bun)', // bun install, run, test + 'Shell(node)', // node scripts + 'Shell(npx)', // npx commands + 'Shell(git)', // git operations (except push) + 'Shell(tsc)', // TypeScript compiler + 'Shell(eslint)', // Linting + 'Shell(prettier)', // Formatting + 'Shell(jest)', // Testing + 'Shell(vitest)', // Testing + 'Shell(cargo)', // Rust + 'Shell(go)', // Go + 'Shell(python)', // Python + 'Shell(pip)', // Python packages + 'Shell(poetry)', // Python packages + 'Shell(make)', // Makefiles + 'Shell(docker)', // Docker (build, not run with --rm) + 'Shell(ls)', // List files + 'Shell(cat)', // Read files + 'Shell(echo)', // Echo + 'Shell(mkdir)', // Create directories + 'Shell(cp)', // Copy files + 'Shell(mv)', // Move files + 'Shell(touch)', // Create files + 'Shell(pwd)', // Print working directory + 'Shell(which)', // Find executables + 'Shell(head)', // Read file head + 'Shell(tail)', // Read file tail + 'Shell(grep)', // Search + 'Shell(find)', // Find files + 'Shell(wc)', // Word count + 'Shell(sort)', // Sort + 'Shell(uniq)', // Unique lines + 'Shell(diff)', // Diff files + 'Shell(curl)', // HTTP requests (read-only fetching) + 'Shell(wget)', // Downloads + ], + deny: [ + // Destructive file operations + 'Shell(rm)', // No file deletion + 'Shell(rmdir)', // No directory deletion + 'Shell(shred)', // No secure delete + + // Dangerous git operations + 'Shell(git push)', // No pushing (user should review) + 'Shell(git push --force)', // Definitely no force push + 'Shell(git reset --hard)', // No hard reset + + // Package publishing + 'Shell(npm publish)', // No publishing packages + 'Shell(pnpm publish)', // No publishing packages + 'Shell(yarn publish)', // No publishing packages + + // System/network operations + 'Shell(sudo)', // No sudo + 'Shell(su)', // No su + 'Shell(chmod)', // No permission changes + 'Shell(chown)', // No ownership changes + 'Shell(kill)', // No process killing + 'Shell(pkill)', // No process killing + 'Shell(killall)', // No process killing + 'Shell(shutdown)', // No shutdown + 'Shell(reboot)', // No reboot + 'Shell(systemctl)', // No systemd + 'Shell(service)', // No services + 'Shell(iptables)', // No firewall + 'Shell(ssh)', // No SSH + 'Shell(scp)', // No SCP + + // Sensitive file access + 'Read(.env*)', // No reading env files + 'Read(**/*.pem)', // No reading private keys + 'Read(**/*.key)', // No reading key files + 'Read(**/credentials*)', // No reading credentials + 'Read(**/.git/config)', // No reading git config (may have tokens) + 'Read(**/id_rsa*)', // No reading SSH keys + 'Read(**/id_ed25519*)', // No reading SSH keys + 'Write(.env*)', // No writing env files + 'Write(**/*.pem)', // No writing keys + 'Write(**/*.key)', // No writing keys + ], + }, +}; + +/** + * All available permission profiles + */ +export const CURSOR_PERMISSION_PROFILES: CursorPermissionProfileConfig[] = [ + CURSOR_STRICT_PROFILE, + CURSOR_DEVELOPMENT_PROFILE, +]; + /** * Cursor authentication status */