mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-02 10:35:37 +08:00
fix: run cursor with --trust if workspace trust prompt is detected, and retry once
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user