import { jsonResponse, errorResponse, corsHeaders } from '../utils/cors' import { existsSync, readdirSync, readFileSync, statSync } from 'fs' import { join } from 'path' import { homedir } from 'os' import { spawn } from 'child_process' import { WORKING_DIR } from '../config' // Agent transcript directories const AGENT_DIRS: Record = { ejecutor: join(WORKING_DIR, '.claude-ejecutor', 'projects'), nucleo000: join(WORKING_DIR, '.claude-nucleo000', 'projects'), claude: join(homedir(), '.claude', 'projects') } // Agent CLI commands (these .cmd wrappers handle CLAUDE_CONFIG_DIR + cd) const AGENT_COMMANDS: Record = { ejecutor: 'ejecutor', nucleo000: 'nucleo000', claude: 'claude' } // Track running processes per session to prevent concurrent sends const runningProcesses = new Map void }>() // Broadcast callback — set by sync-server to push WebSocket messages let broadcastFn: ((msg: string) => void) | null = null export function setTranscriptDebugBroadcast(fn: (msg: string) => void) { broadcastFn = fn } // Project hash for this project const PROJECT_HASH = 'C--Users-jodar-agent-ui' function getProjectDir(agent: string): string | null { const baseDir = AGENT_DIRS[agent] if (!baseDir || !existsSync(baseDir)) return null // Try exact project hash first const exact = join(baseDir, PROJECT_HASH) if (existsSync(exact)) return exact // Fallback: first directory const dirs = readdirSync(baseDir) return dirs.length > 0 ? join(baseDir, dirs[0]) : null } function extractFirstUserMessage(filePath: string): string { try { const content = readFileSync(filePath, 'utf-8') const lines = content.split('\n') for (const line of lines) { if (!line.trim()) continue try { const obj = JSON.parse(line) if (obj.type === 'user' && obj.message) { const c = obj.message.content if (typeof c === 'string') return c.slice(0, 120) if (Array.isArray(c)) { const textBlock = c.find((b: any) => b.type === 'text' && b.text?.trim()) if (textBlock) return textBlock.text.slice(0, 120) } } } catch {} } } catch {} return '' } export function handleTranscriptDebugSessions(url: URL): Response { const agent = url.searchParams.get('agent') || 'ejecutor' const projectDir = getProjectDir(agent) if (!projectDir || !existsSync(projectDir)) { return jsonResponse([]) } const files = readdirSync(projectDir) .filter(f => f.endsWith('.jsonl')) .map(f => { const fullPath = join(projectDir, f) const stat = statSync(fullPath) return { id: f.replace('.jsonl', ''), filename: f, size: stat.size, mtime: stat.mtimeMs, mtimeISO: stat.mtime.toISOString(), firstUserMessage: extractFirstUserMessage(fullPath) } }) .sort((a, b) => b.mtime - a.mtime) return jsonResponse(files) } export function handleTranscriptDebugRaw(sessionId: string, url: URL): Response { const agent = url.searchParams.get('agent') || 'ejecutor' const projectDir = getProjectDir(agent) if (!projectDir) { return errorResponse(`No project directory found for agent: ${agent}`, 404) } const filePath = join(projectDir, `${sessionId}.jsonl`) if (!existsSync(filePath)) { return errorResponse(`Session ${sessionId} not found`, 404) } const content = readFileSync(filePath, 'utf-8') return new Response(content, { status: 200, headers: { 'Content-Type': 'text/plain; charset=utf-8', ...corsHeaders } }) } export function handleTranscriptDebugStatus(url: URL): Response { const sessionId = url.searchParams.get('sessionId') if (!sessionId) return jsonResponse({ processing: false }) return jsonResponse({ processing: runningProcesses.has(sessionId) }) } export async function handleTranscriptDebugSend(req: Request): Promise { if (req.method !== 'POST') { return errorResponse('Method not allowed', 405) } let body: { agent?: string; sessionId?: string; prompt?: string } try { body = await req.json() } catch { return errorResponse('Invalid JSON body') } const { agent = 'ejecutor', sessionId, prompt } = body if (!sessionId) return errorResponse('sessionId is required') if (!prompt?.trim()) return errorResponse('prompt is required') if (!AGENT_COMMANDS[agent]) return errorResponse(`Unknown agent: ${agent}`) // Verify session file exists const projectDir = getProjectDir(agent) if (!projectDir) return errorResponse(`No project directory for agent: ${agent}`, 404) const sessionFile = join(projectDir, `${sessionId}.jsonl`) if (!existsSync(sessionFile)) { return errorResponse(`Session ${sessionId} not found`, 404) } // Prevent concurrent sends to the same session if (runningProcesses.has(sessionId)) { return errorResponse('A prompt is already being processed for this session', 409) } // Use the agent .cmd wrapper directly (handles CLAUDE_CONFIG_DIR + cd) // Escape double quotes for cmd.exe and pass as single command string const agentCmd = AGENT_COMMANDS[agent] const escaped = prompt.replace(/"/g, '""') const cmd = `${agentCmd} --resume "${sessionId}" --permission-mode default -p "${escaped}"` const env = { ...process.env } delete env.CLAUDECODE console.log(`[TranscriptDebug] Spawning: ${agentCmd} --resume ${sessionId.slice(0, 8)}...`) const child = spawn(cmd, { cwd: WORKING_DIR, env, stdio: ['ignore', 'pipe', 'pipe'], shell: true } as any) // Track the running process runningProcesses.set(sessionId, { pid: child.pid || 0, kill: () => child.kill() }) let stderr = '' child.stdout?.on('data', (data: Buffer) => { console.log(`[TranscriptDebug] stdout: ${data.toString().slice(0, 200)}`) }) child.stderr?.on('data', (data: Buffer) => { stderr += data.toString() }) child.on('close', (code) => { runningProcesses.delete(sessionId) if (code !== 0) { console.error(`[TranscriptDebug] claude exited with code ${code}`) if (stderr) console.error(`[TranscriptDebug] stderr: ${stderr.slice(0, 500)}`) } else { console.log(`[TranscriptDebug] claude completed for session ${sessionId.slice(0, 8)}...`) } // Notify frontend via WebSocket if (broadcastFn) { broadcastFn(JSON.stringify({ type: 'transcript-debug-done', sessionId, exitCode: code })) } }) child.on('error', (err) => { runningProcesses.delete(sessionId) console.error(`[TranscriptDebug] Failed to spawn claude:`, err.message) if (broadcastFn) { broadcastFn(JSON.stringify({ type: 'transcript-debug-done', sessionId, error: err.message })) } }) return jsonResponse({ success: true, pid: child.pid, message: `Prompt sent to ${agent} session ${sessionId.slice(0, 8)}...` }) }