fix: API would be stringified twice. That is now fixed.

This commit is contained in:
simosmik
2025-12-29 23:18:38 +00:00
parent 60c8bda755
commit babe96eedd
5 changed files with 84 additions and 54 deletions

View File

@@ -398,10 +398,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
// Send session-created event only once for new sessions // Send session-created event only once for new sessions
if (!sessionId && !sessionCreatedSent) { if (!sessionId && !sessionCreatedSent) {
sessionCreatedSent = true; sessionCreatedSent = true;
ws.send(JSON.stringify({ ws.send({
type: 'session-created', type: 'session-created',
sessionId: capturedSessionId sessionId: capturedSessionId
})); });
} else { } else {
console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent); console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
} }
@@ -411,20 +411,20 @@ async function queryClaudeSDK(command, options = {}, ws) {
// Transform and send message to WebSocket // Transform and send message to WebSocket
const transformedMessage = transformMessage(message); const transformedMessage = transformMessage(message);
ws.send(JSON.stringify({ ws.send({
type: 'claude-response', type: 'claude-response',
data: transformedMessage data: transformedMessage
})); });
// Extract and send token budget updates from result messages // Extract and send token budget updates from result messages
if (message.type === 'result') { if (message.type === 'result') {
const tokenBudget = extractTokenBudget(message); const tokenBudget = extractTokenBudget(message);
if (tokenBudget) { if (tokenBudget) {
console.log('Token budget from modelUsage:', tokenBudget); console.log('Token budget from modelUsage:', tokenBudget);
ws.send(JSON.stringify({ ws.send({
type: 'token-budget', type: 'token-budget',
data: tokenBudget data: tokenBudget
})); });
} }
} }
} }
@@ -439,12 +439,12 @@ async function queryClaudeSDK(command, options = {}, ws) {
// Send completion event // Send completion event
console.log('Streaming complete, sending claude-complete event'); console.log('Streaming complete, sending claude-complete event');
ws.send(JSON.stringify({ ws.send({
type: 'claude-complete', type: 'claude-complete',
sessionId: capturedSessionId, sessionId: capturedSessionId,
exitCode: 0, exitCode: 0,
isNewSession: !sessionId && !!command isNewSession: !sessionId && !!command
})); });
console.log('claude-complete event sent'); console.log('claude-complete event sent');
} catch (error) { } catch (error) {
@@ -459,10 +459,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
await cleanupTempFiles(tempImagePaths, tempDir); await cleanupTempFiles(tempImagePaths, tempDir);
// Send error to WebSocket // Send error to WebSocket
ws.send(JSON.stringify({ ws.send({
type: 'claude-error', type: 'claude-error',
error: error.message error: error.message
})); });
throw error; throw error;
} }

View File

@@ -102,29 +102,29 @@ async function spawnCursor(command, options = {}, ws) {
// Send session-created event only once for new sessions // Send session-created event only once for new sessions
if (!sessionId && !sessionCreatedSent) { if (!sessionId && !sessionCreatedSent) {
sessionCreatedSent = true; sessionCreatedSent = true;
ws.send(JSON.stringify({ ws.send({
type: 'session-created', type: 'session-created',
sessionId: capturedSessionId, sessionId: capturedSessionId,
model: response.model, model: response.model,
cwd: response.cwd cwd: response.cwd
})); });
} }
} }
// Send system info to frontend // Send system info to frontend
ws.send(JSON.stringify({ ws.send({
type: 'cursor-system', type: 'cursor-system',
data: response data: response
})); });
} }
break; break;
case 'user': case 'user':
// Forward user message // Forward user message
ws.send(JSON.stringify({ ws.send({
type: 'cursor-user', type: 'cursor-user',
data: response data: response
})); });
break; break;
case 'assistant': case 'assistant':
@@ -134,7 +134,7 @@ async function spawnCursor(command, options = {}, ws) {
messageBuffer += textContent; messageBuffer += textContent;
// Send as Claude-compatible format for frontend // Send as Claude-compatible format for frontend
ws.send(JSON.stringify({ ws.send({
type: 'claude-response', type: 'claude-response',
data: { data: {
type: 'content_block_delta', type: 'content_block_delta',
@@ -143,7 +143,7 @@ async function spawnCursor(command, options = {}, ws) {
text: textContent text: textContent
} }
} }
})); });
} }
break; break;
@@ -153,37 +153,37 @@ async function spawnCursor(command, options = {}, ws) {
// Send final message if we have buffered content // Send final message if we have buffered content
if (messageBuffer) { if (messageBuffer) {
ws.send(JSON.stringify({ ws.send({
type: 'claude-response', type: 'claude-response',
data: { data: {
type: 'content_block_stop' type: 'content_block_stop'
} }
})); });
} }
// Send completion event // Send completion event
ws.send(JSON.stringify({ ws.send({
type: 'cursor-result', type: 'cursor-result',
sessionId: capturedSessionId || sessionId, sessionId: capturedSessionId || sessionId,
data: response, data: response,
success: response.subtype === 'success' success: response.subtype === 'success'
})); });
break; break;
default: default:
// Forward any other message types // Forward any other message types
ws.send(JSON.stringify({ ws.send({
type: 'cursor-response', type: 'cursor-response',
data: response data: response
})); });
} }
} catch (parseError) { } catch (parseError) {
console.log('📄 Non-JSON response:', line); console.log('📄 Non-JSON response:', line);
// If not JSON, send as raw text // If not JSON, send as raw text
ws.send(JSON.stringify({ ws.send({
type: 'cursor-output', type: 'cursor-output',
data: line data: line
})); });
} }
} }
}); });
@@ -191,10 +191,10 @@ async function spawnCursor(command, options = {}, ws) {
// Handle stderr // Handle stderr
cursorProcess.stderr.on('data', (data) => { cursorProcess.stderr.on('data', (data) => {
console.error('Cursor CLI stderr:', data.toString()); console.error('Cursor CLI stderr:', data.toString());
ws.send(JSON.stringify({ ws.send({
type: 'cursor-error', type: 'cursor-error',
error: data.toString() error: data.toString()
})); });
}); });
// Handle process completion // Handle process completion
@@ -205,12 +205,12 @@ async function spawnCursor(command, options = {}, ws) {
const finalSessionId = capturedSessionId || sessionId || processKey; const finalSessionId = capturedSessionId || sessionId || processKey;
activeCursorProcesses.delete(finalSessionId); activeCursorProcesses.delete(finalSessionId);
ws.send(JSON.stringify({ ws.send({
type: 'claude-complete', type: 'claude-complete',
sessionId: finalSessionId, sessionId: finalSessionId,
exitCode: code, exitCode: code,
isNewSession: !sessionId && !!command // Flag to indicate this was a new session isNewSession: !sessionId && !!command // Flag to indicate this was a new session
})); });
if (code === 0) { if (code === 0) {
resolve(); resolve();
@@ -227,10 +227,10 @@ async function spawnCursor(command, options = {}, ws) {
const finalSessionId = capturedSessionId || sessionId || processKey; const finalSessionId = capturedSessionId || sessionId || processKey;
activeCursorProcesses.delete(finalSessionId); activeCursorProcesses.delete(finalSessionId);
ws.send(JSON.stringify({ ws.send({
type: 'cursor-error', type: 'cursor-error',
error: error.message error: error.message
})); });
reject(error); reject(error);
}); });

View File

@@ -717,6 +717,32 @@ wss.on('connection', (ws, request) => {
} }
}); });
/**
* WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
*/
class WebSocketWriter {
constructor(ws) {
this.ws = ws;
this.sessionId = null;
this.isWebSocketWriter = true; // Marker for transport detection
}
send(data) {
if (this.ws.readyState === 1) { // WebSocket.OPEN
// Providers send raw objects, we stringify for WebSocket
this.ws.send(JSON.stringify(data));
}
}
setSessionId(sessionId) {
this.sessionId = sessionId;
}
getSessionId() {
return this.sessionId;
}
}
// Handle chat WebSocket connections // Handle chat WebSocket connections
function handleChatConnection(ws) { function handleChatConnection(ws) {
console.log('[INFO] Chat WebSocket connected'); console.log('[INFO] Chat WebSocket connected');
@@ -724,6 +750,9 @@ function handleChatConnection(ws) {
// Add to connected clients for project updates // Add to connected clients for project updates
connectedClients.add(ws); connectedClients.add(ws);
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
const writer = new WebSocketWriter(ws);
ws.on('message', async (message) => { ws.on('message', async (message) => {
try { try {
const data = JSON.parse(message); const data = JSON.parse(message);
@@ -734,19 +763,19 @@ function handleChatConnection(ws) {
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
// Use Claude Agents SDK // Use Claude Agents SDK
await queryClaudeSDK(data.command, data.options, ws); await queryClaudeSDK(data.command, data.options, writer);
} else if (data.type === 'cursor-command') { } else if (data.type === 'cursor-command') {
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]'); console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
console.log('📁 Project:', data.options?.cwd || 'Unknown'); console.log('📁 Project:', data.options?.cwd || 'Unknown');
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
console.log('🤖 Model:', data.options?.model || 'default'); console.log('🤖 Model:', data.options?.model || 'default');
await spawnCursor(data.command, data.options, ws); await spawnCursor(data.command, data.options, writer);
} else if (data.type === 'codex-command') { } else if (data.type === 'codex-command') {
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]'); console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown'); console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
console.log('🤖 Model:', data.options?.model || 'default'); console.log('🤖 Model:', data.options?.model || 'default');
await queryCodex(data.command, data.options, ws); await queryCodex(data.command, data.options, writer);
} else if (data.type === 'cursor-resume') { } else if (data.type === 'cursor-resume') {
// Backward compatibility: treat as cursor-command with resume and no prompt // Backward compatibility: treat as cursor-command with resume and no prompt
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId); console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
@@ -754,7 +783,7 @@ function handleChatConnection(ws) {
sessionId: data.sessionId, sessionId: data.sessionId,
resume: true, resume: true,
cwd: data.options?.cwd cwd: data.options?.cwd
}, ws); }, writer);
} else if (data.type === 'abort-session') { } else if (data.type === 'abort-session') {
console.log('[DEBUG] Abort session request:', data.sessionId); console.log('[DEBUG] Abort session request:', data.sessionId);
const provider = data.provider || 'claude'; const provider = data.provider || 'claude';
@@ -769,21 +798,21 @@ function handleChatConnection(ws) {
success = await abortClaudeSDKSession(data.sessionId); success = await abortClaudeSDKSession(data.sessionId);
} }
ws.send(JSON.stringify({ writer.send({
type: 'session-aborted', type: 'session-aborted',
sessionId: data.sessionId, sessionId: data.sessionId,
provider, provider,
success success
})); });
} else if (data.type === 'cursor-abort') { } else if (data.type === 'cursor-abort') {
console.log('[DEBUG] Abort Cursor session:', data.sessionId); console.log('[DEBUG] Abort Cursor session:', data.sessionId);
const success = abortCursorSession(data.sessionId); const success = abortCursorSession(data.sessionId);
ws.send(JSON.stringify({ writer.send({
type: 'session-aborted', type: 'session-aborted',
sessionId: data.sessionId, sessionId: data.sessionId,
provider: 'cursor', provider: 'cursor',
success success
})); });
} else if (data.type === 'check-session-status') { } else if (data.type === 'check-session-status') {
// Check if a specific session is currently processing // Check if a specific session is currently processing
const provider = data.provider || 'claude'; const provider = data.provider || 'claude';
@@ -799,12 +828,12 @@ function handleChatConnection(ws) {
isActive = isClaudeSDKSessionActive(sessionId); isActive = isClaudeSDKSessionActive(sessionId);
} }
ws.send(JSON.stringify({ writer.send({
type: 'session-status', type: 'session-status',
sessionId, sessionId,
provider, provider,
isProcessing: isActive isProcessing: isActive
})); });
} else if (data.type === 'get-active-sessions') { } else if (data.type === 'get-active-sessions') {
// Get all currently active sessions // Get all currently active sessions
const activeSessions = { const activeSessions = {
@@ -812,17 +841,17 @@ function handleChatConnection(ws) {
cursor: getActiveCursorSessions(), cursor: getActiveCursorSessions(),
codex: getActiveCodexSessions() codex: getActiveCodexSessions()
}; };
ws.send(JSON.stringify({ writer.send({
type: 'active-sessions', type: 'active-sessions',
sessions: activeSessions sessions: activeSessions
})); });
} }
} catch (error) { } catch (error) {
console.error('[ERROR] Chat WebSocket error:', error.message); console.error('[ERROR] Chat WebSocket error:', error.message);
ws.send(JSON.stringify({ writer.send({
type: 'error', type: 'error',
error: error.message error: error.message
})); });
} }
}); });

View File

@@ -360,12 +360,12 @@ export function getActiveCodexSessions() {
*/ */
function sendMessage(ws, data) { function sendMessage(ws, data) {
try { try {
if (typeof ws.send === 'function') { if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
// WebSocket // Writer handles stringification (SSEStreamWriter or WebSocketWriter)
ws.send(data);
} else if (typeof ws.send === 'function') {
// Raw WebSocket - stringify here
ws.send(JSON.stringify(data)); ws.send(JSON.stringify(data));
} else if (typeof ws.write === 'function') {
// SSE writer (for agent API)
ws.write(`data: ${JSON.stringify(data)}\n\n`);
} }
} catch (error) { } catch (error) {
console.error('[Codex] Error sending message:', error); console.error('[Codex] Error sending message:', error);

View File

@@ -451,6 +451,7 @@ class SSEStreamWriter {
constructor(res) { constructor(res) {
this.res = res; this.res = res;
this.sessionId = null; this.sessionId = null;
this.isSSEStreamWriter = true; // Marker for transport detection
} }
send(data) { send(data) {
@@ -458,7 +459,7 @@ class SSEStreamWriter {
return; return;
} }
// Format as SSE // Format as SSE - providers send raw objects, we stringify
this.res.write(`data: ${JSON.stringify(data)}\n\n`); this.res.write(`data: ${JSON.stringify(data)}\n\n`);
} }