mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-10 09:09:37 +00:00
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
267 lines
8.9 KiB
JavaScript
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
|
|
}; |