Files
claudecodeui/server/cursor-cli.js
simos eda89ef147 feat(api): add API for one-shot prompt generatio, key authentication system and git commit message generation
Implement comprehensive API key management functionality including
generation, validation, and CRUD operations.

Changes:
- Add API key database schema and operations (create, validate, delete,
  toggle)
- Generating a commit message will now work properly with claude sdk and cursor cli and return a suggested commit message
- Implement crypto-based key generation with 'ck_' prefix
- Add session ID tracking in claude-sdk.js and cursor-cli.js
- Update database layer with API key validation and last_used tracking
- Support multi-user API key management with user association

This enables secure programmatic access to the agent service
2025-10-30 20:59:25 +00:00

267 lines
8.9 KiB
JavaScript

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
async function spawnCursor(command, options = {}, ws) {
return new Promise(async (resolve, reject) => {
const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, images } = 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
// Use tools settings passed from frontend, or defaults
const settings = toolsSettings || {
allowedShellCommands: [],
skipPermissions: false
};
// Build Cursor CLI command
const args = [];
// 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);
}
if (command && command.trim()) {
// Provide a prompt (works for both new and resumed sessions)
args.push('-p', command);
// Add model flag if specified (only meaningful for new sessions; harmless on resume)
if (!sessionId && model) {
args.push('--model', model);
}
// Request streaming JSON when we are providing a prompt
args.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)');
}
// 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);
}
// Send session-created event only once for new sessions
if (!sessionId && !sessionCreatedSent) {
sessionCreatedSent = true;
ws.send(JSON.stringify({
type: 'session-created',
sessionId: capturedSessionId,
model: response.model,
cwd: response.cwd
}));
}
}
// Send system info to frontend
ws.send(JSON.stringify({
type: 'cursor-system',
data: response
}));
}
break;
case 'user':
// Forward user message
ws.send(JSON.stringify({
type: 'cursor-user',
data: response
}));
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(JSON.stringify({
type: 'claude-response',
data: {
type: 'content_block_delta',
delta: {
type: 'text_delta',
text: textContent
}
}
}));
}
break;
case 'result':
// Session complete
console.log('Cursor session result:', response);
// Send final message if we have buffered content
if (messageBuffer) {
ws.send(JSON.stringify({
type: 'claude-response',
data: {
type: 'content_block_stop'
}
}));
}
// Send completion event
ws.send(JSON.stringify({
type: 'cursor-result',
sessionId: capturedSessionId || sessionId,
data: response,
success: response.subtype === 'success'
}));
break;
default:
// Forward any other message types
ws.send(JSON.stringify({
type: 'cursor-response',
data: response
}));
}
} catch (parseError) {
console.log('📄 Non-JSON response:', line);
// If not JSON, send as raw text
ws.send(JSON.stringify({
type: 'cursor-output',
data: line
}));
}
}
});
// Handle stderr
cursorProcess.stderr.on('data', (data) => {
console.error('Cursor CLI stderr:', data.toString());
ws.send(JSON.stringify({
type: 'cursor-error',
error: data.toString()
}));
});
// 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(JSON.stringify({
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(JSON.stringify({
type: 'cursor-error',
error: error.message
}));
reject(error);
});
// Close stdin since Cursor doesn't need interactive input
cursorProcess.stdin.end();
});
}
function abortCursorSession(sessionId) {
const process = activeCursorProcesses.get(sessionId);
if (process) {
console.log(`🛑 Aborting Cursor session: ${sessionId}`);
process.kill('SIGTERM');
activeCursorProcesses.delete(sessionId);
return true;
}
return false;
}
function isCursorSessionActive(sessionId) {
return activeCursorProcesses.has(sessionId);
}
function getActiveCursorSessions() {
return Array.from(activeCursorProcesses.keys());
}
export {
spawnCursor,
abortCursorSession,
isCursorSessionActive,
getActiveCursorSessions
};