← back to Letsbegin
app/api/live/route.ts
242 lines
import { NextResponse } from 'next/server'
import { exec } from 'child_process'
import { promisify } from 'util'
import * as fs from 'fs'
import * as path from 'path'
const execAsync = promisify(exec)
interface ClaudeProcess {
pid: number
cpu: string
memory: string
terminal: string
uptime: string
command: string
type: 'interactive' | 'ralph' | 'mcp' | 'unknown'
project?: string
status: string
}
interface RalphStatus {
running: boolean
project: string
projectPath: string
branch: string
currentStory: string
progress: string
completedStories: string[]
pendingStories: string[]
runningStory: string | null
logs: string[]
elapsed: number
storyDetails: Array<{
id: string
title: string
status: 'done' | 'running' | 'pending' | 'fail'
time?: number
}>
}
export async function GET() {
try {
// Get all Claude processes
const { stdout: psOutput } = await execAsync(
`ps aux | grep -E "claude|anthropic" | grep -v grep || true`
)
const claudeProcesses: ClaudeProcess[] = []
const lines = psOutput.trim().split('\n').filter(Boolean)
for (const line of lines) {
const parts = line.split(/\s+/)
if (parts.length < 11) continue
const pid = parseInt(parts[1])
const cpu = parts[2]
const mem = parts[3]
const terminal = parts[6]
const command = parts.slice(10).join(' ')
// Determine type
let type: ClaudeProcess['type'] = 'unknown'
let project = ''
if (command.includes('--claude-in-chrome-mcp')) {
type = 'mcp'
} else if (parts[0] === 'ralph') {
type = 'ralph'
// Try to get project from ralph
try {
const { stdout } = await execAsync(`ls -la /proc/${pid}/cwd 2>/dev/null || true`)
const match = stdout.match(/-> (.+)/)
if (match) project = match[1]
} catch {}
} else if (command === 'claude' || command.includes('claude-code')) {
type = 'interactive'
}
// Get uptime
let uptime = 'unknown'
try {
const { stdout } = await execAsync(`ps -o etime= -p ${pid} 2>/dev/null || true`)
uptime = stdout.trim() || 'unknown'
} catch {}
claudeProcesses.push({
pid,
cpu,
memory: mem,
terminal,
uptime,
command: command.slice(0, 100),
type,
project,
status: 'running'
})
}
// Get Ralph status for all projects with progress.txt
const ralphProjects: RalphStatus[] = []
// Scan for projects with Ralph progress files
const projectDirs = [
'/root/Projects/jill-website',
'/root/Projects/angels-flowers',
'/root/Projects/Designer-Wallcoverings',
'/root/Projects/dear-bubbe-nextjs',
'/root/Projects/Letsbegin'
]
for (const projectPath of projectDirs) {
try {
const progressPath = path.join(projectPath, 'scripts/ralph/progress.txt')
const prdPath = path.join(projectPath, 'scripts/ralph/prd.json')
if (!fs.existsSync(progressPath)) continue
const progressContent = fs.readFileSync(progressPath, 'utf-8')
const lines = progressContent.split('\n')
// Parse progress.txt header
let branch = ''
let startTime = ''
const branchMatch = progressContent.match(/# Branch: (.+)/)
const startMatch = progressContent.match(/# Started: (.+)/)
if (branchMatch) branch = branchMatch[1]
if (startMatch) startTime = startMatch[1]
// Parse story statuses directly from progress.txt
const storyResults: Record<string, 'done' | 'running' | 'pending' | 'fail'> = {}
const storyDetails: RalphStatus['storyDetails'] = []
for (const line of lines) {
const match = line.match(/^(US-\d+):\s*(DONE|PASS|FAIL|RUNNING|PENDING)\s*-?\s*(.*)/)
if (match) {
const storyId = match[1]
const status = match[2]
const title = match[3] || storyId
let mappedStatus: 'done' | 'running' | 'pending' | 'fail' = 'pending'
if (status === 'DONE' || status === 'PASS') mappedStatus = 'done'
else if (status === 'FAIL') mappedStatus = 'fail'
else if (status === 'RUNNING') mappedStatus = 'running'
storyResults[storyId] = mappedStatus
storyDetails.push({ id: storyId, title, status: mappedStatus })
}
}
// If PRD exists, get full titles
if (fs.existsSync(prdPath)) {
try {
const prd = JSON.parse(fs.readFileSync(prdPath, 'utf-8'))
for (const story of prd.userStories || []) {
const existing = storyDetails.find(s => s.id === story.id)
if (existing) {
existing.title = story.title
}
}
} catch {}
}
const completed = storyDetails.filter(s => s.status === 'done').length
const total = storyDetails.length
const running = storyDetails.find(s => s.status === 'running')
const isRunning = running !== undefined
// Check if Ralph process is actually running for this project
let hasActiveProcess = false
try {
const { stdout } = await execAsync(`ps aux | grep ralph | grep -v grep | grep "${path.basename(projectPath)}" || true`)
hasActiveProcess = stdout.trim().length > 0
} catch {}
// Also check the log file for recent activity
const logPath = '/tmp/ralph-output.log'
let logLines: string[] = []
if (fs.existsSync(logPath)) {
const logContent = fs.readFileSync(logPath, 'utf-8')
logLines = logContent.split('\n').slice(-50)
}
ralphProjects.push({
running: isRunning || hasActiveProcess,
project: path.basename(projectPath),
projectPath,
branch,
currentStory: running?.id || storyDetails[storyDetails.length - 1]?.id || '',
progress: `${completed}/${total}`,
completedStories: storyDetails.filter(s => s.status === 'done').map(s => s.id),
pendingStories: storyDetails.filter(s => s.status === 'pending').map(s => s.id),
runningStory: running?.id || null,
logs: logLines,
elapsed: startTime ? Math.floor((Date.now() - new Date(startTime).getTime()) / 1000) : 0,
storyDetails
})
} catch (err) {
console.error(`Failed to read Ralph status for ${projectPath}:`, err)
}
}
// Use the first project as primary ralph status (for backwards compatibility)
const ralphStatus = ralphProjects.length > 0 ? ralphProjects[0] : null
// Get system stats
let systemStats = {
loadAvg: [0, 0, 0],
memUsed: 0,
memTotal: 0,
uptime: ''
}
try {
const { stdout: loadAvg } = await execAsync('cat /proc/loadavg')
const loads = loadAvg.trim().split(' ')
systemStats.loadAvg = [parseFloat(loads[0]), parseFloat(loads[1]), parseFloat(loads[2])]
const { stdout: memInfo } = await execAsync('free -m | grep Mem')
const memParts = memInfo.trim().split(/\s+/)
systemStats.memTotal = parseInt(memParts[1])
systemStats.memUsed = parseInt(memParts[2])
const { stdout: uptime } = await execAsync('uptime -p')
systemStats.uptime = uptime.trim()
} catch {}
return NextResponse.json({
timestamp: new Date().toISOString(),
claudeProcesses,
ralphStatus,
ralphProjects, // All projects with Ralph progress
systemStats,
totalProcesses: claudeProcesses.length,
activeRalph: ralphProjects.some(p => p.running)
})
} catch (error) {
console.error('Live API error:', error)
return NextResponse.json({ error: 'Failed to get live status' }, { status: 500 })
}
}