feat(server): Add readOnly mode to Cursor provider for read-only operations

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 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-30 15:31:35 +01:00
parent 38d0e4103a
commit b0f83b7c76
10 changed files with 21 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = '';

View File

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

View File

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

View File

@@ -97,6 +97,7 @@ async function executeWithCursor(prompt: string, model: string): Promise<string>
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) {

View File

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

View File

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