mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-09 03:39:37 +00:00
feat: Enhance session management and tool settings for Claude and Cursor
- Updated ClaudeStatus component to accept a provider prop for better flexibility. - Added CursorLogo component for displaying cursor sessions. - Modified MainContent to conditionally display session names based on provider. - Updated Shell component to show session names and summaries based on provider. - Enhanced Sidebar to handle both Claude and Cursor sessions, including sorting and displaying session icons. - Introduced new ToolsSettings functionality to manage tools for both Claude and Cursor, including allowed and disallowed commands. - Implemented fetching and saving of Cursor-specific settings and commands. - Added UI elements for managing Cursor tools, including permission settings and command lists.
This commit is contained in:
250
server/cursor-cli.js
Normal file
250
server/cursor-cli.js
Normal file
@@ -0,0 +1,250 @@
|
||||
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)
|
||||
if (resume && sessionId) {
|
||||
// Resume existing session
|
||||
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 (!resume && 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);
|
||||
}
|
||||
|
||||
// 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',
|
||||
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',
|
||||
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;
|
||||
}
|
||||
|
||||
export {
|
||||
spawnCursor,
|
||||
abortCursorSession
|
||||
};
|
||||
@@ -38,9 +38,11 @@ import mime from 'mime-types';
|
||||
|
||||
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
||||
import { spawnClaude, abortClaudeSession } from './claude-cli.js';
|
||||
import { spawnCursor, abortCursorSession } from './cursor-cli.js';
|
||||
import gitRoutes from './routes/git.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
import mcpRoutes from './routes/mcp.js';
|
||||
import cursorRoutes from './routes/cursor.js';
|
||||
import { initializeDatabase } from './database/db.js';
|
||||
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
||||
|
||||
@@ -175,6 +177,9 @@ app.use('/api/git', authenticateToken, gitRoutes);
|
||||
// MCP API Routes (protected)
|
||||
app.use('/api/mcp', authenticateToken, mcpRoutes);
|
||||
|
||||
// Cursor API Routes (protected)
|
||||
app.use('/api/cursor', authenticateToken, cursorRoutes);
|
||||
|
||||
// Static files served after API routes
|
||||
app.use(express.static(path.join(__dirname, '../dist')));
|
||||
|
||||
@@ -460,12 +465,39 @@ function handleChatConnection(ws) {
|
||||
console.log('📁 Project:', data.options?.projectPath || 'Unknown');
|
||||
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
||||
await spawnClaude(data.command, data.options, ws);
|
||||
} else if (data.type === 'cursor-command') {
|
||||
console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]');
|
||||
console.log('📁 Project:', data.options?.cwd || 'Unknown');
|
||||
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
||||
console.log('🤖 Model:', data.options?.model || 'default');
|
||||
await spawnCursor(data.command, data.options, ws);
|
||||
} else if (data.type === 'cursor-resume') {
|
||||
// Backward compatibility: treat as cursor-command with resume and no prompt
|
||||
console.log('🖱️ Cursor resume session (compat):', data.sessionId);
|
||||
await spawnCursor('', {
|
||||
sessionId: data.sessionId,
|
||||
resume: true,
|
||||
cwd: data.options?.cwd
|
||||
}, ws);
|
||||
} else if (data.type === 'abort-session') {
|
||||
console.log('🛑 Abort session request:', data.sessionId);
|
||||
const success = abortClaudeSession(data.sessionId);
|
||||
const provider = data.provider || 'claude';
|
||||
const success = provider === 'cursor'
|
||||
? abortCursorSession(data.sessionId)
|
||||
: abortClaudeSession(data.sessionId);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session-aborted',
|
||||
sessionId: data.sessionId,
|
||||
provider,
|
||||
success
|
||||
}));
|
||||
} else if (data.type === 'cursor-abort') {
|
||||
console.log('🛑 Abort Cursor session:', data.sessionId);
|
||||
const success = abortCursorSession(data.sessionId);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session-aborted',
|
||||
sessionId: data.sessionId,
|
||||
provider: 'cursor',
|
||||
success
|
||||
}));
|
||||
}
|
||||
|
||||
637
server/routes/cursor.js
Normal file
637
server/routes/cursor.js
Normal file
@@ -0,0 +1,637 @@
|
||||
import express from 'express';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { spawn } from 'child_process';
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { open } from 'sqlite';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET /api/cursor/config - Read Cursor CLI configuration
|
||||
router.get('/config', async (req, res) => {
|
||||
try {
|
||||
const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json');
|
||||
|
||||
try {
|
||||
const configContent = await fs.readFile(configPath, 'utf8');
|
||||
const config = JSON.parse(configContent);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
config: config,
|
||||
path: configPath
|
||||
});
|
||||
} catch (error) {
|
||||
// Config doesn't exist or is invalid
|
||||
console.log('Cursor config not found or invalid:', error.message);
|
||||
|
||||
// Return default config
|
||||
res.json({
|
||||
success: true,
|
||||
config: {
|
||||
version: 1,
|
||||
model: {
|
||||
modelId: "gpt-5",
|
||||
displayName: "GPT-5"
|
||||
},
|
||||
permissions: {
|
||||
allow: [],
|
||||
deny: []
|
||||
}
|
||||
},
|
||||
isDefault: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading Cursor config:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to read Cursor configuration',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/cursor/config - Update Cursor CLI configuration
|
||||
router.post('/config', async (req, res) => {
|
||||
try {
|
||||
const { permissions, model } = req.body;
|
||||
const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json');
|
||||
|
||||
// Read existing config or create default
|
||||
let config = {
|
||||
version: 1,
|
||||
editor: {
|
||||
vimMode: false
|
||||
},
|
||||
hasChangedDefaultModel: false,
|
||||
privacyCache: {
|
||||
ghostMode: false,
|
||||
privacyMode: 3,
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const existing = await fs.readFile(configPath, 'utf8');
|
||||
config = JSON.parse(existing);
|
||||
} catch (error) {
|
||||
// Config doesn't exist, use defaults
|
||||
console.log('Creating new Cursor config');
|
||||
}
|
||||
|
||||
// Update permissions if provided
|
||||
if (permissions) {
|
||||
config.permissions = {
|
||||
allow: permissions.allow || [],
|
||||
deny: permissions.deny || []
|
||||
};
|
||||
}
|
||||
|
||||
// Update model if provided
|
||||
if (model) {
|
||||
config.model = model;
|
||||
config.hasChangedDefaultModel = true;
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
const configDir = path.dirname(configPath);
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
|
||||
// Write updated config
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
config: config,
|
||||
message: 'Cursor configuration updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating Cursor config:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to update Cursor configuration',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/cursor/mcp - Read Cursor MCP servers configuration
|
||||
router.get('/mcp', async (req, res) => {
|
||||
try {
|
||||
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
||||
|
||||
try {
|
||||
const mcpContent = await fs.readFile(mcpPath, 'utf8');
|
||||
const mcpConfig = JSON.parse(mcpContent);
|
||||
|
||||
// Convert to UI-friendly format
|
||||
const servers = [];
|
||||
if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') {
|
||||
for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
|
||||
const server = {
|
||||
id: name,
|
||||
name: name,
|
||||
type: 'stdio',
|
||||
scope: 'cursor',
|
||||
config: {},
|
||||
raw: config
|
||||
};
|
||||
|
||||
// Determine transport type and extract config
|
||||
if (config.command) {
|
||||
server.type = 'stdio';
|
||||
server.config.command = config.command;
|
||||
server.config.args = config.args || [];
|
||||
server.config.env = config.env || {};
|
||||
} else if (config.url) {
|
||||
server.type = config.transport || 'http';
|
||||
server.config.url = config.url;
|
||||
server.config.headers = config.headers || {};
|
||||
}
|
||||
|
||||
servers.push(server);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
servers: servers,
|
||||
path: mcpPath
|
||||
});
|
||||
} catch (error) {
|
||||
// MCP config doesn't exist
|
||||
console.log('Cursor MCP config not found:', error.message);
|
||||
res.json({
|
||||
success: true,
|
||||
servers: [],
|
||||
isDefault: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading Cursor MCP config:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to read Cursor MCP configuration',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/cursor/mcp/add - Add MCP server to Cursor configuration
|
||||
router.post('/mcp/add', async (req, res) => {
|
||||
try {
|
||||
const { name, type = 'stdio', command, args = [], url, headers = {}, env = {} } = req.body;
|
||||
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
||||
|
||||
console.log(`➕ Adding MCP server to Cursor config: ${name}`);
|
||||
|
||||
// Read existing config or create new
|
||||
let mcpConfig = { mcpServers: {} };
|
||||
|
||||
try {
|
||||
const existing = await fs.readFile(mcpPath, 'utf8');
|
||||
mcpConfig = JSON.parse(existing);
|
||||
if (!mcpConfig.mcpServers) {
|
||||
mcpConfig.mcpServers = {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Creating new Cursor MCP config');
|
||||
}
|
||||
|
||||
// Build server config based on type
|
||||
let serverConfig = {};
|
||||
|
||||
if (type === 'stdio') {
|
||||
serverConfig = {
|
||||
command: command,
|
||||
args: args,
|
||||
env: env
|
||||
};
|
||||
} else if (type === 'http' || type === 'sse') {
|
||||
serverConfig = {
|
||||
url: url,
|
||||
transport: type,
|
||||
headers: headers
|
||||
};
|
||||
}
|
||||
|
||||
// Add server to config
|
||||
mcpConfig.mcpServers[name] = serverConfig;
|
||||
|
||||
// Ensure directory exists
|
||||
const mcpDir = path.dirname(mcpPath);
|
||||
await fs.mkdir(mcpDir, { recursive: true });
|
||||
|
||||
// Write updated config
|
||||
await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `MCP server "${name}" added to Cursor configuration`,
|
||||
config: mcpConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error adding MCP server to Cursor:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to add MCP server',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/cursor/mcp/:name - Remove MCP server from Cursor configuration
|
||||
router.delete('/mcp/:name', async (req, res) => {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
||||
|
||||
console.log(`🗑️ Removing MCP server from Cursor config: ${name}`);
|
||||
|
||||
// Read existing config
|
||||
let mcpConfig = { mcpServers: {} };
|
||||
|
||||
try {
|
||||
const existing = await fs.readFile(mcpPath, 'utf8');
|
||||
mcpConfig = JSON.parse(existing);
|
||||
} catch (error) {
|
||||
return res.status(404).json({
|
||||
error: 'Cursor MCP configuration not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if server exists
|
||||
if (!mcpConfig.mcpServers || !mcpConfig.mcpServers[name]) {
|
||||
return res.status(404).json({
|
||||
error: `MCP server "${name}" not found in Cursor configuration`
|
||||
});
|
||||
}
|
||||
|
||||
// Remove server from config
|
||||
delete mcpConfig.mcpServers[name];
|
||||
|
||||
// Write updated config
|
||||
await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `MCP server "${name}" removed from Cursor configuration`,
|
||||
config: mcpConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error removing MCP server from Cursor:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to remove MCP server',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/cursor/mcp/add-json - Add MCP server using JSON format
|
||||
router.post('/mcp/add-json', async (req, res) => {
|
||||
try {
|
||||
const { name, jsonConfig } = req.body;
|
||||
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
||||
|
||||
console.log(`➕ Adding MCP server to Cursor config via JSON: ${name}`);
|
||||
|
||||
// Validate and parse JSON config
|
||||
let parsedConfig;
|
||||
try {
|
||||
parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig;
|
||||
} catch (parseError) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid JSON configuration',
|
||||
details: parseError.message
|
||||
});
|
||||
}
|
||||
|
||||
// Read existing config or create new
|
||||
let mcpConfig = { mcpServers: {} };
|
||||
|
||||
try {
|
||||
const existing = await fs.readFile(mcpPath, 'utf8');
|
||||
mcpConfig = JSON.parse(existing);
|
||||
if (!mcpConfig.mcpServers) {
|
||||
mcpConfig.mcpServers = {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Creating new Cursor MCP config');
|
||||
}
|
||||
|
||||
// Add server to config
|
||||
mcpConfig.mcpServers[name] = parsedConfig;
|
||||
|
||||
// Ensure directory exists
|
||||
const mcpDir = path.dirname(mcpPath);
|
||||
await fs.mkdir(mcpDir, { recursive: true });
|
||||
|
||||
// Write updated config
|
||||
await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `MCP server "${name}" added to Cursor configuration via JSON`,
|
||||
config: mcpConfig
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error adding MCP server to Cursor via JSON:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to add MCP server',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/cursor/sessions - Get Cursor sessions from SQLite database
|
||||
router.get('/sessions', async (req, res) => {
|
||||
try {
|
||||
const { projectPath } = req.query;
|
||||
|
||||
// Calculate cwdID hash for the project path (Cursor uses MD5 hash)
|
||||
const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
|
||||
const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
|
||||
|
||||
console.log(`🔍 Looking for Cursor sessions in: ${cursorChatsPath}`);
|
||||
|
||||
// Check if the directory exists
|
||||
try {
|
||||
await fs.access(cursorChatsPath);
|
||||
} catch (error) {
|
||||
// No sessions for this project
|
||||
return res.json({
|
||||
success: true,
|
||||
sessions: [],
|
||||
cwdId: cwdId,
|
||||
path: cursorChatsPath
|
||||
});
|
||||
}
|
||||
|
||||
// List all session directories
|
||||
const sessionDirs = await fs.readdir(cursorChatsPath);
|
||||
const sessions = [];
|
||||
|
||||
for (const sessionId of sessionDirs) {
|
||||
const sessionPath = path.join(cursorChatsPath, sessionId);
|
||||
const storeDbPath = path.join(sessionPath, 'store.db');
|
||||
|
||||
try {
|
||||
// Check if store.db exists
|
||||
await fs.access(storeDbPath);
|
||||
|
||||
// Open SQLite database
|
||||
const db = await open({
|
||||
filename: storeDbPath,
|
||||
driver: sqlite3.Database,
|
||||
mode: sqlite3.OPEN_READONLY
|
||||
});
|
||||
|
||||
// Get metadata from meta table
|
||||
const metaRows = await db.all(`
|
||||
SELECT key, value FROM meta
|
||||
`);
|
||||
|
||||
let sessionData = {
|
||||
id: sessionId,
|
||||
name: 'Untitled Session',
|
||||
createdAt: null,
|
||||
mode: null,
|
||||
projectPath: projectPath,
|
||||
lastMessage: null,
|
||||
messageCount: 0
|
||||
};
|
||||
|
||||
// Parse meta table entries
|
||||
for (const row of metaRows) {
|
||||
if (row.value) {
|
||||
try {
|
||||
// Try to decode as hex-encoded JSON
|
||||
const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
|
||||
if (hexMatch) {
|
||||
const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
|
||||
const data = JSON.parse(jsonStr);
|
||||
|
||||
if (row.key === 'agent') {
|
||||
sessionData.name = data.name || sessionData.name;
|
||||
sessionData.createdAt = data.createdAt;
|
||||
sessionData.mode = data.mode;
|
||||
sessionData.agentId = data.agentId;
|
||||
sessionData.latestRootBlobId = data.latestRootBlobId;
|
||||
}
|
||||
} else {
|
||||
// If not hex, use raw value for simple keys
|
||||
if (row.key === 'name') {
|
||||
sessionData.name = row.value.toString();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Could not parse meta value for key ${row.key}:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get message count from blobs table
|
||||
try {
|
||||
const blobCount = await db.get(`
|
||||
SELECT COUNT(*) as count FROM blobs
|
||||
`);
|
||||
sessionData.messageCount = blobCount.count;
|
||||
|
||||
// Get the most recent blob for preview
|
||||
const lastBlob = await db.get(`
|
||||
SELECT data FROM blobs
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
if (lastBlob && lastBlob.data) {
|
||||
try {
|
||||
// Try to extract readable preview from blob (may contain binary with embedded JSON)
|
||||
const raw = lastBlob.data.toString('utf8');
|
||||
let preview = '';
|
||||
// Attempt direct JSON parse
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed?.content) {
|
||||
if (Array.isArray(parsed.content)) {
|
||||
const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || '';
|
||||
preview = firstText;
|
||||
} else if (typeof parsed.content === 'string') {
|
||||
preview = parsed.content;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
if (!preview) {
|
||||
// Strip non-printable and try to find JSON chunk
|
||||
const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
|
||||
const s = cleaned;
|
||||
const start = s.indexOf('{');
|
||||
const end = s.lastIndexOf('}');
|
||||
if (start !== -1 && end > start) {
|
||||
const jsonStr = s.slice(start, end + 1);
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
if (parsed?.content) {
|
||||
if (Array.isArray(parsed.content)) {
|
||||
const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || '';
|
||||
preview = firstText;
|
||||
} else if (typeof parsed.content === 'string') {
|
||||
preview = parsed.content;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
preview = s;
|
||||
}
|
||||
} else {
|
||||
preview = s;
|
||||
}
|
||||
}
|
||||
if (preview && preview.length > 0) {
|
||||
sessionData.lastMessage = preview.substring(0, 100) + (preview.length > 100 ? '...' : '');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not parse blob data:', e.message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not read blobs:', e.message);
|
||||
}
|
||||
|
||||
await db.close();
|
||||
|
||||
sessions.push(sessionData);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`Could not read session ${sessionId}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort sessions by creation date (newest first)
|
||||
sessions.sort((a, b) => {
|
||||
if (!a.createdAt) return 1;
|
||||
if (!b.createdAt) return -1;
|
||||
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sessions: sessions,
|
||||
cwdId: cwdId,
|
||||
path: cursorChatsPath
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reading Cursor sessions:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to read Cursor sessions',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/cursor/sessions/:sessionId - Get specific Cursor session from SQLite
|
||||
router.get('/sessions/:sessionId', async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { projectPath } = req.query;
|
||||
|
||||
// Calculate cwdID hash for the project path
|
||||
const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
|
||||
const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db');
|
||||
|
||||
console.log(`📖 Reading Cursor session from: ${storeDbPath}`);
|
||||
|
||||
// Open SQLite database
|
||||
const db = await open({
|
||||
filename: storeDbPath,
|
||||
driver: sqlite3.Database,
|
||||
mode: sqlite3.OPEN_READONLY
|
||||
});
|
||||
|
||||
// Get all blobs (conversation data)
|
||||
const blobs = await db.all(`
|
||||
SELECT id, data FROM blobs
|
||||
ORDER BY id ASC
|
||||
`);
|
||||
|
||||
// Get metadata from meta table
|
||||
const metaRows = await db.all(`
|
||||
SELECT key, value FROM meta
|
||||
`);
|
||||
|
||||
// Parse metadata
|
||||
let metadata = {};
|
||||
for (const row of metaRows) {
|
||||
if (row.value) {
|
||||
try {
|
||||
// Try to decode as hex-encoded JSON
|
||||
const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
|
||||
if (hexMatch) {
|
||||
const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
|
||||
metadata[row.key] = JSON.parse(jsonStr);
|
||||
} else {
|
||||
metadata[row.key] = row.value.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
metadata[row.key] = row.value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse blob data to extract messages
|
||||
const messages = [];
|
||||
for (const blob of blobs) {
|
||||
try {
|
||||
// Attempt direct JSON parse first
|
||||
const raw = blob.data.toString('utf8');
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (_) {
|
||||
// If not JSON, try to extract JSON from within binary-looking string
|
||||
const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
|
||||
const start = cleaned.indexOf('{');
|
||||
const end = cleaned.lastIndexOf('}');
|
||||
if (start !== -1 && end > start) {
|
||||
const jsonStr = cleaned.slice(start, end + 1);
|
||||
try {
|
||||
parsed = JSON.parse(jsonStr);
|
||||
} catch (_) {
|
||||
parsed = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed) {
|
||||
messages.push({ id: blob.id, content: parsed });
|
||||
} else {
|
||||
// Fallback to cleaned text content
|
||||
const text = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '').trim();
|
||||
messages.push({ id: blob.id, content: text });
|
||||
}
|
||||
} catch (e) {
|
||||
messages.push({ id: blob.id, content: blob.data.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
await db.close();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
session: {
|
||||
id: sessionId,
|
||||
projectPath: projectPath,
|
||||
messages: messages,
|
||||
metadata: metadata,
|
||||
cwdId: cwdId
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reading Cursor session:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to read Cursor session',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user