fix: run cursor with --trust if workspace trust prompt is detected, and retry once

This commit is contained in:
Haileyesus
2026-03-10 22:54:19 +03:00
parent 52d4671504
commit 3e617394b7

View File

@@ -1,20 +1,34 @@
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import crossSpawn from 'cross-spawn'; import crossSpawn from 'cross-spawn';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
// Use cross-spawn on Windows for better command execution // Use cross-spawn on Windows for better command execution
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn; const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
let activeCursorProcesses = new Map(); // Track active processes by session ID let activeCursorProcesses = new Map(); // Track active processes by session ID
const WORKSPACE_TRUST_PATTERNS = [
/workspace trust required/i,
/do you trust the contents of this directory/i,
/working with untrusted contents/i,
/pass --trust,\s*--yolo,\s*or -f/i
];
function isWorkspaceTrustPrompt(text = '') {
if (!text || typeof text !== 'string') {
return false;
}
return WORKSPACE_TRUST_PATTERNS.some((pattern) => pattern.test(text));
}
async function spawnCursor(command, options = {}, ws) { async function spawnCursor(command, options = {}, ws) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, images } = options; const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model } = options;
let capturedSessionId = sessionId; // Track session ID throughout the process let capturedSessionId = sessionId; // Track session ID throughout the process
let sessionCreatedSent = false; // Track if we've already sent session-created event let sessionCreatedSent = false; // Track if we've already sent session-created event
let messageBuffer = ''; // Buffer for accumulating assistant messages let messageBuffer = ''; // Buffer for accumulating assistant messages
let hasRetriedWithTrust = false;
let settled = false;
// Use tools settings passed from frontend, or defaults // Use tools settings passed from frontend, or defaults
const settings = toolsSettings || { const settings = toolsSettings || {
@@ -23,235 +37,288 @@ async function spawnCursor(command, options = {}, ws) {
}; };
// Build Cursor CLI command // Build Cursor CLI command
const args = []; const baseArgs = [];
// Build flags allowing both resume and prompt together (reply in existing session) // Build flags allowing both resume and prompt together (reply in existing session)
// Treat presence of sessionId as intention to resume, regardless of resume flag // Treat presence of sessionId as intention to resume, regardless of resume flag
if (sessionId) { if (sessionId) {
args.push('--resume=' + sessionId); baseArgs.push('--resume=' + sessionId);
} }
if (command && command.trim()) { if (command && command.trim()) {
// Provide a prompt (works for both new and resumed sessions) // Provide a prompt (works for both new and resumed sessions)
args.push('-p', command); baseArgs.push('-p', command);
// Add model flag if specified (only meaningful for new sessions; harmless on resume) // Add model flag if specified (only meaningful for new sessions; harmless on resume)
if (!sessionId && model) { if (!sessionId && model) {
args.push('--model', model); baseArgs.push('--model', model);
} }
// Request streaming JSON when we are providing a prompt // Request streaming JSON when we are providing a prompt
args.push('--output-format', 'stream-json'); baseArgs.push('--output-format', 'stream-json');
} }
// Add skip permissions flag if enabled // Add skip permissions flag if enabled
if (skipPermissions || settings.skipPermissions) { if (skipPermissions || settings.skipPermissions) {
args.push('-f'); baseArgs.push('-f');
console.log('⚠️ Using -f flag (skip permissions)'); console.log('Using -f flag (skip permissions)');
} }
// Use cwd (actual project directory) instead of projectPath // Use cwd (actual project directory) instead of projectPath
const workingDir = cwd || projectPath || process.cwd(); const workingDir = cwd || projectPath || process.cwd();
console.log('Spawning Cursor CLI:', 'cursor-agent', args.join(' '));
console.log('Working directory:', workingDir);
console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
const cursorProcess = spawnFunction('cursor-agent', args, {
cwd: workingDir,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env } // Inherit all environment variables
});
// Store process reference for potential abort // Store process reference for potential abort
const processKey = capturedSessionId || Date.now().toString(); const processKey = capturedSessionId || Date.now().toString();
activeCursorProcesses.set(processKey, cursorProcess);
// Handle stdout (streaming JSON responses) const settleOnce = (callback) => {
cursorProcess.stdout.on('data', (data) => { if (settled) {
const rawOutput = data.toString(); return;
console.log('📤 Cursor CLI stdout:', rawOutput); }
settled = true;
callback();
};
const lines = rawOutput.split('\n').filter(line => line.trim()); const runCursorProcess = (args, runReason = 'initial') => {
const isTrustRetry = runReason === 'trust-retry';
let runSawWorkspaceTrustPrompt = false;
for (const line of lines) { if (isTrustRetry) {
try { console.log('Retrying Cursor CLI with --trust after workspace trust prompt');
const response = JSON.parse(line); }
console.log('📄 Parsed JSON response:', response);
// Handle different message types console.log('Spawning Cursor CLI:', 'cursor-agent', args.join(' '));
switch (response.type) { console.log('Working directory:', workingDir);
case 'system': console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
if (response.subtype === 'init') {
// Capture session ID
if (response.session_id && !capturedSessionId) {
capturedSessionId = response.session_id;
console.log('📝 Captured session ID:', capturedSessionId);
// Update process key with captured session ID const cursorProcess = spawnFunction('cursor-agent', args, {
if (processKey !== capturedSessionId) { cwd: workingDir,
activeCursorProcesses.delete(processKey); stdio: ['pipe', 'pipe', 'pipe'],
activeCursorProcesses.set(capturedSessionId, cursorProcess); env: { ...process.env } // Inherit all environment variables
});
activeCursorProcesses.set(processKey, cursorProcess);
const shouldSuppressForTrustRetry = (text) => {
if (hasRetriedWithTrust || args.includes('--trust')) {
return false;
}
if (!isWorkspaceTrustPrompt(text)) {
return false;
}
runSawWorkspaceTrustPrompt = true;
return true;
};
// Handle stdout (streaming JSON responses)
cursorProcess.stdout.on('data', (data) => {
const rawOutput = data.toString();
console.log('Cursor CLI stdout:', rawOutput);
const lines = rawOutput.split('\n').filter((line) => line.trim());
for (const line of lines) {
try {
const response = JSON.parse(line);
console.log('Parsed JSON response:', response);
// Handle different message types
switch (response.type) {
case 'system':
if (response.subtype === 'init') {
// Capture session ID
if (response.session_id && !capturedSessionId) {
capturedSessionId = response.session_id;
console.log('Captured session ID:', capturedSessionId);
// Update process key with captured session ID
if (processKey !== capturedSessionId) {
activeCursorProcesses.delete(processKey);
activeCursorProcesses.set(capturedSessionId, cursorProcess);
}
// Set session ID on writer (for API endpoint compatibility)
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
ws.setSessionId(capturedSessionId);
}
// Send session-created event only once for new sessions
if (!sessionId && !sessionCreatedSent) {
sessionCreatedSent = true;
ws.send({
type: 'session-created',
sessionId: capturedSessionId,
model: response.model,
cwd: response.cwd
});
}
} }
// Set session ID on writer (for API endpoint compatibility) // Send system info to frontend
if (ws.setSessionId && typeof ws.setSessionId === 'function') { ws.send({
ws.setSessionId(capturedSessionId); type: 'cursor-system',
} data: response,
sessionId: capturedSessionId || sessionId || null
// Send session-created event only once for new sessions });
if (!sessionId && !sessionCreatedSent) {
sessionCreatedSent = true;
ws.send({
type: 'session-created',
sessionId: capturedSessionId,
model: response.model,
cwd: response.cwd
});
}
} }
break;
// Send system info to frontend case 'user':
// Forward user message
ws.send({ ws.send({
type: 'cursor-system', type: 'cursor-user',
data: response, data: response,
sessionId: capturedSessionId || sessionId || null sessionId: capturedSessionId || sessionId || null
}); });
} break;
break;
case 'user': case 'assistant':
// Forward user message // Accumulate assistant message chunks
ws.send({ if (response.message && response.message.content && response.message.content.length > 0) {
type: 'cursor-user', const textContent = response.message.content[0].text;
data: response, messageBuffer += textContent;
sessionId: capturedSessionId || sessionId || null
});
break;
case 'assistant': // Send as Claude-compatible format for frontend
// Accumulate assistant message chunks ws.send({
if (response.message && response.message.content && response.message.content.length > 0) { type: 'claude-response',
const textContent = response.message.content[0].text; data: {
messageBuffer += textContent; type: 'content_block_delta',
delta: {
type: 'text_delta',
text: textContent
}
},
sessionId: capturedSessionId || sessionId || null
});
}
break;
// Send as Claude-compatible format for frontend case 'result':
// Session complete
console.log('Cursor session result:', response);
// Send final message if we have buffered content
if (messageBuffer) {
ws.send({
type: 'claude-response',
data: {
type: 'content_block_stop'
},
sessionId: capturedSessionId || sessionId || null
});
}
// Send completion event
ws.send({ ws.send({
type: 'claude-response', type: 'cursor-result',
data: { sessionId: capturedSessionId || sessionId,
type: 'content_block_delta', data: response,
delta: { success: response.subtype === 'success'
type: 'text_delta', });
text: textContent break;
}
}, default:
// Forward any other message types
ws.send({
type: 'cursor-response',
data: response,
sessionId: capturedSessionId || sessionId || null sessionId: capturedSessionId || sessionId || null
}); });
} }
break; } catch (parseError) {
console.log('Non-JSON response:', line);
case 'result': if (shouldSuppressForTrustRetry(line)) {
// Session complete continue;
console.log('Cursor session result:', response); }
// Send final message if we have buffered content // If not JSON, send as raw text
if (messageBuffer) { ws.send({
ws.send({ type: 'cursor-output',
type: 'claude-response', data: line,
data: { sessionId: capturedSessionId || sessionId || null
type: 'content_block_stop' });
},
sessionId: capturedSessionId || sessionId || null
});
}
// Send completion event
ws.send({
type: 'cursor-result',
sessionId: capturedSessionId || sessionId,
data: response,
success: response.subtype === 'success'
});
break;
default:
// Forward any other message types
ws.send({
type: 'cursor-response',
data: response,
sessionId: capturedSessionId || sessionId || null
});
} }
} catch (parseError) {
console.log('📄 Non-JSON response:', line);
// If not JSON, send as raw text
ws.send({
type: 'cursor-output',
data: line,
sessionId: capturedSessionId || sessionId || null
});
} }
}
});
// Handle stderr
cursorProcess.stderr.on('data', (data) => {
console.error('Cursor CLI stderr:', data.toString());
ws.send({
type: 'cursor-error',
error: data.toString(),
sessionId: capturedSessionId || sessionId || null
});
});
// Handle process completion
cursorProcess.on('close', async (code) => {
console.log(`Cursor CLI process exited with code ${code}`);
// Clean up process reference
const finalSessionId = capturedSessionId || sessionId || processKey;
activeCursorProcesses.delete(finalSessionId);
ws.send({
type: 'claude-complete',
sessionId: finalSessionId,
exitCode: code,
isNewSession: !sessionId && !!command // Flag to indicate this was a new session
}); });
if (code === 0) { // Handle stderr
resolve(); cursorProcess.stderr.on('data', (data) => {
} else { const stderrText = data.toString();
reject(new Error(`Cursor CLI exited with code ${code}`)); console.error('Cursor CLI stderr:', stderrText);
}
});
// Handle process errors if (shouldSuppressForTrustRetry(stderrText)) {
cursorProcess.on('error', (error) => { return;
console.error('Cursor CLI process error:', error); }
// Clean up process reference on error ws.send({
const finalSessionId = capturedSessionId || sessionId || processKey; type: 'cursor-error',
activeCursorProcesses.delete(finalSessionId); error: stderrText,
sessionId: capturedSessionId || sessionId || null
ws.send({ });
type: 'cursor-error',
error: error.message,
sessionId: capturedSessionId || sessionId || null
}); });
reject(error); // Handle process completion
}); cursorProcess.on('close', async (code) => {
console.log(`Cursor CLI process exited with code ${code}`);
// Close stdin since Cursor doesn't need interactive input const finalSessionId = capturedSessionId || sessionId || processKey;
cursorProcess.stdin.end(); activeCursorProcesses.delete(finalSessionId);
if (
runSawWorkspaceTrustPrompt &&
code !== 0 &&
!hasRetriedWithTrust &&
!args.includes('--trust')
) {
hasRetriedWithTrust = true;
runCursorProcess([...args, '--trust'], 'trust-retry');
return;
}
ws.send({
type: 'claude-complete',
sessionId: finalSessionId,
exitCode: code,
isNewSession: !sessionId && !!command // Flag to indicate this was a new session
});
if (code === 0) {
settleOnce(() => resolve());
} else {
settleOnce(() => reject(new Error(`Cursor CLI exited with code ${code}`)));
}
});
// Handle process errors
cursorProcess.on('error', (error) => {
console.error('Cursor CLI process error:', error);
// Clean up process reference on error
const finalSessionId = capturedSessionId || sessionId || processKey;
activeCursorProcesses.delete(finalSessionId);
ws.send({
type: 'cursor-error',
error: error.message,
sessionId: capturedSessionId || sessionId || null
});
settleOnce(() => reject(error));
});
// Close stdin since Cursor doesn't need interactive input
cursorProcess.stdin.end();
};
runCursorProcess(baseArgs, 'initial');
}); });
} }
function abortCursorSession(sessionId) { function abortCursorSession(sessionId) {
const process = activeCursorProcesses.get(sessionId); const process = activeCursorProcesses.get(sessionId);
if (process) { if (process) {
console.log(`🛑 Aborting Cursor session: ${sessionId}`); console.log(`Aborting Cursor session: ${sessionId}`);
process.kill('SIGTERM'); process.kill('SIGTERM');
activeCursorProcesses.delete(sessionId); activeCursorProcesses.delete(sessionId);
return true; return true;