nyk
feat(refactor): ready for manual QA after main sync (#274)
b6ecafa unverified
import { NextRequest, NextResponse } from 'next/server'
import { existsSync, readdirSync, statSync } from 'fs'
import path from 'path'
import Database from 'better-sqlite3'
import { config } from '@/lib/config'
import { requireRole } from '@/lib/auth'
import { readLimiter } from '@/lib/rate-limit'
import { logger } from '@/lib/logger'
interface AgentFileInfo {
path: string
chunks: number
textSize: number
}
interface AgentGraphData {
name: string
dbSize: number
totalChunks: number
totalFiles: number
files: AgentFileInfo[]
}
const memoryDbDir = config.openclawStateDir
? path.join(config.openclawStateDir, 'memory')
: ''
function getAgentData(dbPath: string, agentName: string): AgentGraphData | null {
try {
const dbStat = statSync(dbPath)
const db = new Database(dbPath, { readonly: true, fileMustExist: true })
let files: AgentFileInfo[] = []
let totalChunks = 0
let totalFiles = 0
try {
// Check if chunks table exists
const tableCheck = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'")
.get() as { name: string } | undefined
if (tableCheck) {
// Use COUNT only — skip SUM(LENGTH(text)) which forces a full data scan
const rows = db
.prepare(
'SELECT path, COUNT(*) as chunks FROM chunks GROUP BY path ORDER BY chunks DESC'
)
.all() as Array<{ path: string; chunks: number }>
files = rows.map((r) => ({
path: r.path || '(unknown)',
chunks: r.chunks,
textSize: 0,
}))
totalChunks = files.reduce((sum, f) => sum + f.chunks, 0)
totalFiles = files.length
}
} finally {
db.close()
}
return {
name: agentName,
dbSize: dbStat.size,
totalChunks,
totalFiles,
files,
}
} catch (err) {
logger.warn(`Failed to read memory DB for agent "${agentName}": ${err}`)
return null
}
}
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const limited = readLimiter(request)
if (limited) return limited
if (!memoryDbDir || !existsSync(memoryDbDir)) {
return NextResponse.json(
{ error: 'Memory directory not available', agents: [] },
{ status: 404 }
)
}
const agentFilter = request.nextUrl.searchParams.get('agent') || 'all'
try {
const entries = readdirSync(memoryDbDir).filter((f) => f.endsWith('.sqlite'))
const agents: AgentGraphData[] = []
for (const entry of entries) {
const agentName = entry.replace('.sqlite', '')
if (agentFilter !== 'all' && agentName !== agentFilter) continue
const dbPath = path.join(memoryDbDir, entry)
const data = getAgentData(dbPath, agentName)
if (data) agents.push(data)
}
// Sort by total chunks descending
agents.sort((a, b) => b.totalChunks - a.totalChunks)
return NextResponse.json({ agents })
} catch (err) {
logger.error(`Failed to build memory graph data: ${err}`)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}