From b0f83b7c76548a01801ba8a727506b0e47a99342 Mon Sep 17 00:00:00 2001 From: Kacper Date: Tue, 30 Dec 2025 15:31:35 +0100 Subject: [PATCH] feat(server): Add readOnly mode to Cursor provider for read-only operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a readOnly option to ExecuteOptions that controls whether the Cursor CLI runs with --force flag (allows edits) or without (suggest-only). Read-only routes now pass readOnly: true: - generate-spec.ts, generate-features-from-spec.ts (we write files ourselves) - validate-issue.ts, generate-suggestions.ts (analysis only) - describe-file.ts, describe-image.ts (description only) - generate-plan.ts, enhance.ts (text generation only) Routes that implement features (auto-mode-service, agent-service) keep the default (readOnly: false) to allow file modifications. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/server/src/providers/cursor-provider.ts | 8 +++++++- apps/server/src/providers/types.ts | 6 ++++++ .../src/routes/app-spec/generate-features-from-spec.ts | 1 + apps/server/src/routes/app-spec/generate-spec.ts | 1 + apps/server/src/routes/backlog-plan/generate-plan.ts | 1 + apps/server/src/routes/context/routes/describe-file.ts | 1 + apps/server/src/routes/context/routes/describe-image.ts | 1 + apps/server/src/routes/enhance-prompt/routes/enhance.ts | 1 + apps/server/src/routes/github/routes/validate-issue.ts | 1 + .../server/src/routes/suggestions/generate-suggestions.ts | 1 + 10 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/server/src/providers/cursor-provider.ts b/apps/server/src/providers/cursor-provider.ts index 14effe8e..941eb967 100644 --- a/apps/server/src/providers/cursor-provider.ts +++ b/apps/server/src/providers/cursor-provider.ts @@ -140,12 +140,18 @@ export class CursorProvider extends CliProvider { // shell escaping issues when content contains $(), backticks, etc. const cliArgs: string[] = [ '-p', // Print mode (non-interactive) - '--force', // Allow file modifications '--output-format', 'stream-json', '--stream-partial-output', // Real-time streaming ]; + // Only add --force if NOT in read-only mode + // Without --force, Cursor CLI suggests changes but doesn't apply them + // With --force, Cursor CLI can actually edit files + if (!options.readOnly) { + cliArgs.push('--force'); + } + // Add model if not auto if (model !== 'auto') { cliArgs.push('--model', model); diff --git a/apps/server/src/providers/types.ts b/apps/server/src/providers/types.ts index 5fcdb3e6..2d7bfebe 100644 --- a/apps/server/src/providers/types.ts +++ b/apps/server/src/providers/types.ts @@ -34,6 +34,12 @@ export interface ExecuteOptions { conversationHistory?: ConversationMessage[]; // Previous messages for context sdkSessionId?: string; // Claude SDK session ID for resuming conversations settingSources?: Array<'user' | 'project' | 'local'>; // Claude filesystem settings to load + /** + * If true, the provider should run in read-only mode (no file modifications). + * For Cursor CLI, this omits the --force flag, making it suggest-only. + * Default: false (allows edits) + */ + readOnly?: boolean; } /** diff --git a/apps/server/src/routes/app-spec/generate-features-from-spec.ts b/apps/server/src/routes/app-spec/generate-features-from-spec.ts index 8b8e4a4a..8aea59ba 100644 --- a/apps/server/src/routes/app-spec/generate-features-from-spec.ts +++ b/apps/server/src/routes/app-spec/generate-features-from-spec.ts @@ -132,6 +132,7 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge maxTurns: 250, allowedTools: ['Read', 'Glob', 'Grep'], abortController, + readOnly: true, // Feature generation only reads code, doesn't write })) { messageCount++; diff --git a/apps/server/src/routes/app-spec/generate-spec.ts b/apps/server/src/routes/app-spec/generate-spec.ts index 87ccf1e7..98985c1e 100644 --- a/apps/server/src/routes/app-spec/generate-spec.ts +++ b/apps/server/src/routes/app-spec/generate-spec.ts @@ -132,6 +132,7 @@ ${JSON.stringify(specOutputSchema, null, 2)}`; maxTurns: 250, allowedTools: ['Read', 'Glob', 'Grep'], abortController, + readOnly: true, // Spec generation only reads code, we write the spec ourselves })) { messageCount++; diff --git a/apps/server/src/routes/backlog-plan/generate-plan.ts b/apps/server/src/routes/backlog-plan/generate-plan.ts index 78d6f141..8ff58b68 100644 --- a/apps/server/src/routes/backlog-plan/generate-plan.ts +++ b/apps/server/src/routes/backlog-plan/generate-plan.ts @@ -183,6 +183,7 @@ Please analyze the current backlog and the user's request, then provide a JSON p allowedTools: [], // No tools needed for this abortController, settingSources: autoLoadClaudeMd ? ['user', 'project'] : undefined, + readOnly: true, // Plan generation only generates text, doesn't write files }); let responseText = ''; diff --git a/apps/server/src/routes/context/routes/describe-file.ts b/apps/server/src/routes/context/routes/describe-file.ts index 3b30347c..08da54ca 100644 --- a/apps/server/src/routes/context/routes/describe-file.ts +++ b/apps/server/src/routes/context/routes/describe-file.ts @@ -207,6 +207,7 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`; cwd, maxTurns: 1, allowedTools: [], + readOnly: true, // File description only reads, doesn't write })) { if (msg.type === 'assistant' && msg.message?.content) { for (const block of msg.message.content) { diff --git a/apps/server/src/routes/context/routes/describe-image.ts b/apps/server/src/routes/context/routes/describe-image.ts index 3b87bd7b..c445a243 100644 --- a/apps/server/src/routes/context/routes/describe-image.ts +++ b/apps/server/src/routes/context/routes/describe-image.ts @@ -371,6 +371,7 @@ export function createDescribeImageHandler( cwd, maxTurns: 1, allowedTools: ['Read'], // Allow Read tool so Cursor can read the image if needed + readOnly: true, // Image description only reads, doesn't write })) { if (msg.type === 'assistant' && msg.message?.content) { for (const block of msg.message.content) { diff --git a/apps/server/src/routes/enhance-prompt/routes/enhance.ts b/apps/server/src/routes/enhance-prompt/routes/enhance.ts index 3cbea851..30a3b1a0 100644 --- a/apps/server/src/routes/enhance-prompt/routes/enhance.ts +++ b/apps/server/src/routes/enhance-prompt/routes/enhance.ts @@ -97,6 +97,7 @@ async function executeWithCursor(prompt: string, model: string): Promise prompt, model, cwd: process.cwd(), // Enhancement doesn't need a specific working directory + readOnly: true, // Prompt enhancement only generates text, doesn't write files })) { if (msg.type === 'assistant' && msg.message?.content) { for (const block of msg.message.content) { diff --git a/apps/server/src/routes/github/routes/validate-issue.ts b/apps/server/src/routes/github/routes/validate-issue.ts index 29f55ed2..b51435b8 100644 --- a/apps/server/src/routes/github/routes/validate-issue.ts +++ b/apps/server/src/routes/github/routes/validate-issue.ts @@ -111,6 +111,7 @@ ${prompt}`; prompt: cursorPrompt, model, cwd: projectPath, + readOnly: true, // Issue validation only reads code, doesn't write })) { if (msg.type === 'assistant' && msg.message?.content) { for (const block of msg.message.content) { diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index a0eb60f1..fd4c6a50 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -202,6 +202,7 @@ ${JSON.stringify(suggestionsSchema, null, 2)}`; maxTurns: 250, allowedTools: ['Read', 'Glob', 'Grep'], abortController, + readOnly: true, // Suggestions only reads code, doesn't write })) { if (msg.type === 'assistant' && msg.message?.content) { for (const block of msg.message.content) {