nyk
feat: audit hardening, webhook retry, and local Claude session tracking (#68)
959f8d7 unverified
Raw
History Blame Contribute Delete
3.48 kB
import { NextRequest, NextResponse } from 'next/server'
import { getDatabase } from '@/lib/db'
import { requireRole } from '@/lib/auth'
import { syncClaudeSessions } from '@/lib/claude-sessions'
import { logger } from '@/lib/logger'
/**
* GET /api/claude/sessions β€” List discovered local Claude Code sessions
*
* Query params:
* active=1 β€” only active sessions
* project=slug β€” filter by project slug
* limit=50 β€” max results (default 50, max 200)
* offset=0 β€” pagination offset
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
try {
const db = getDatabase()
const { searchParams } = new URL(request.url)
const active = searchParams.get('active')
const project = searchParams.get('project')
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 200)
const offset = parseInt(searchParams.get('offset') || '0')
let query = 'SELECT * FROM claude_sessions WHERE 1=1'
const params: any[] = []
if (active === '1') {
query += ' AND is_active = 1'
}
if (project) {
query += ' AND project_slug = ?'
params.push(project)
}
query += ' ORDER BY last_message_at DESC LIMIT ? OFFSET ?'
params.push(limit, offset)
const sessions = db.prepare(query).all(...params)
// Get total count
let countQuery = 'SELECT COUNT(*) as total FROM claude_sessions WHERE 1=1'
const countParams: any[] = []
if (active === '1') {
countQuery += ' AND is_active = 1'
}
if (project) {
countQuery += ' AND project_slug = ?'
countParams.push(project)
}
const { total } = db.prepare(countQuery).get(...countParams) as { total: number }
// Aggregate stats
const stats = db.prepare(`
SELECT
COUNT(*) as total_sessions,
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_sessions,
SUM(input_tokens) as total_input_tokens,
SUM(output_tokens) as total_output_tokens,
SUM(estimated_cost) as total_estimated_cost,
COUNT(DISTINCT project_slug) as unique_projects
FROM claude_sessions
`).get() as any
return NextResponse.json({
sessions,
total,
stats: {
total_sessions: stats.total_sessions || 0,
active_sessions: stats.active_sessions || 0,
total_input_tokens: stats.total_input_tokens || 0,
total_output_tokens: stats.total_output_tokens || 0,
total_estimated_cost: Math.round((stats.total_estimated_cost || 0) * 100) / 100,
unique_projects: stats.unique_projects || 0,
},
})
} catch (error) {
logger.error({ err: error }, 'GET /api/claude/sessions error')
return NextResponse.json({ error: 'Failed to fetch Claude sessions' }, { status: 500 })
}
}
/**
* POST /api/claude/sessions β€” Trigger a manual scan of local Claude sessions
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'operator')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
try {
const result = await syncClaudeSessions()
return NextResponse.json(result)
} catch (error) {
logger.error({ err: error }, 'POST /api/claude/sessions error')
return NextResponse.json({ error: 'Failed to scan Claude sessions' }, { status: 500 })
}
}