mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-12 01:17:48 +00:00
fix: run cursor with --trust if workspace trust prompt is detected, and retry once
This commit is contained in:
@@ -1,257 +1,324 @@
|
||||
import { spawn } from 'child_process';
|
||||
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
|
||||
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
||||
|
||||
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) {
|
||||
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 sessionCreatedSent = false; // Track if we've already sent session-created event
|
||||
let messageBuffer = ''; // Buffer for accumulating assistant messages
|
||||
|
||||
let hasRetriedWithTrust = false;
|
||||
let settled = false;
|
||||
|
||||
// Use tools settings passed from frontend, or defaults
|
||||
const settings = toolsSettings || {
|
||||
allowedShellCommands: [],
|
||||
skipPermissions: false
|
||||
};
|
||||
|
||||
|
||||
// Build Cursor CLI command
|
||||
const args = [];
|
||||
|
||||
const baseArgs = [];
|
||||
|
||||
// Build flags allowing both resume and prompt together (reply in existing session)
|
||||
// Treat presence of sessionId as intention to resume, regardless of resume flag
|
||||
if (sessionId) {
|
||||
args.push('--resume=' + sessionId);
|
||||
baseArgs.push('--resume=' + sessionId);
|
||||
}
|
||||
|
||||
if (command && command.trim()) {
|
||||
// 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)
|
||||
if (!sessionId && model) {
|
||||
args.push('--model', model);
|
||||
baseArgs.push('--model', model);
|
||||
}
|
||||
|
||||
// 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
|
||||
if (skipPermissions || settings.skipPermissions) {
|
||||
args.push('-f');
|
||||
console.log('⚠️ Using -f flag (skip permissions)');
|
||||
baseArgs.push('-f');
|
||||
console.log('Using -f flag (skip permissions)');
|
||||
}
|
||||
|
||||
|
||||
// Use cwd (actual project directory) instead of projectPath
|
||||
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
|
||||
const processKey = capturedSessionId || Date.now().toString();
|
||||
activeCursorProcesses.set(processKey, cursorProcess);
|
||||
|
||||
// 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);
|
||||
|
||||
const settleOnce = (callback) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
callback();
|
||||
};
|
||||
|
||||
const runCursorProcess = (args, runReason = 'initial') => {
|
||||
const isTrustRetry = runReason === 'trust-retry';
|
||||
let runSawWorkspaceTrustPrompt = false;
|
||||
|
||||
if (isTrustRetry) {
|
||||
console.log('Retrying Cursor CLI with --trust after workspace trust prompt');
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
// Send system info to frontend
|
||||
ws.send({
|
||||
type: 'cursor-system',
|
||||
data: response,
|
||||
sessionId: capturedSessionId || sessionId || null
|
||||
});
|
||||
}
|
||||
|
||||
// Send system info to frontend
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
// Forward user message
|
||||
ws.send({
|
||||
type: 'cursor-system',
|
||||
type: 'cursor-user',
|
||||
data: response,
|
||||
sessionId: capturedSessionId || sessionId || null
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
// Forward user message
|
||||
ws.send({
|
||||
type: 'cursor-user',
|
||||
data: response,
|
||||
sessionId: capturedSessionId || sessionId || null
|
||||
});
|
||||
break;
|
||||
|
||||
case 'assistant':
|
||||
// Accumulate assistant message chunks
|
||||
if (response.message && response.message.content && response.message.content.length > 0) {
|
||||
const textContent = response.message.content[0].text;
|
||||
messageBuffer += textContent;
|
||||
|
||||
// Send as Claude-compatible format for frontend
|
||||
break;
|
||||
|
||||
case 'assistant':
|
||||
// Accumulate assistant message chunks
|
||||
if (response.message && response.message.content && response.message.content.length > 0) {
|
||||
const textContent = response.message.content[0].text;
|
||||
messageBuffer += textContent;
|
||||
|
||||
// Send as Claude-compatible format for frontend
|
||||
ws.send({
|
||||
type: 'claude-response',
|
||||
data: {
|
||||
type: 'content_block_delta',
|
||||
delta: {
|
||||
type: 'text_delta',
|
||||
text: textContent
|
||||
}
|
||||
},
|
||||
sessionId: capturedSessionId || sessionId || null
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
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({
|
||||
type: 'claude-response',
|
||||
data: {
|
||||
type: 'content_block_delta',
|
||||
delta: {
|
||||
type: 'text_delta',
|
||||
text: textContent
|
||||
}
|
||||
},
|
||||
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
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
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({
|
||||
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 (shouldSuppressForTrustRetry(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If not JSON, send as raw text
|
||||
ws.send({
|
||||
type: 'cursor-output',
|
||||
data: line,
|
||||
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) {
|
||||
resolve();
|
||||
} else {
|
||||
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
|
||||
});
|
||||
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Close stdin since Cursor doesn't need interactive input
|
||||
cursorProcess.stdin.end();
|
||||
// Handle stderr
|
||||
cursorProcess.stderr.on('data', (data) => {
|
||||
const stderrText = data.toString();
|
||||
console.error('Cursor CLI stderr:', stderrText);
|
||||
|
||||
if (shouldSuppressForTrustRetry(stderrText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send({
|
||||
type: 'cursor-error',
|
||||
error: stderrText,
|
||||
sessionId: capturedSessionId || sessionId || null
|
||||
});
|
||||
});
|
||||
|
||||
// Handle process completion
|
||||
cursorProcess.on('close', async (code) => {
|
||||
console.log(`Cursor CLI process exited with code ${code}`);
|
||||
|
||||
const finalSessionId = capturedSessionId || sessionId || processKey;
|
||||
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) {
|
||||
const process = activeCursorProcesses.get(sessionId);
|
||||
if (process) {
|
||||
console.log(`🛑 Aborting Cursor session: ${sessionId}`);
|
||||
console.log(`Aborting Cursor session: ${sessionId}`);
|
||||
process.kill('SIGTERM');
|
||||
activeCursorProcesses.delete(sessionId);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user