mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
improve: enhance cleanup-sessions script with better preview output and message filtering
- Improved preview output columns: Removed id and tag columns, added Title (from metadata.summary.text), First Message (first user message), and human-readable Updated date - Reordered columns for clarity: Updated, Msgs (fixed width), Title, First Message, Path - Added --message=PATTERN filter option for fuzzy matching on first user message (case-insensitive) - Fixed deletion count to use actual session count instead of result.changes (which included CASCADE deleted messages) - Added helper functions: formatDate(), truncate(), extractUserText()
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* Supports filtering by:
|
||||
* - Message count: Delete sessions with fewer than N messages
|
||||
* - Path pattern: Delete sessions matching a glob pattern
|
||||
* - Message pattern: Delete sessions whose first message contains a pattern
|
||||
* - Orphaned: Delete sessions whose path no longer exists
|
||||
*
|
||||
* Usage:
|
||||
@@ -13,6 +14,7 @@
|
||||
* Options:
|
||||
* --min-messages=N Delete sessions with fewer than N messages (default: 5)
|
||||
* --path=PATTERN Delete sessions matching path pattern (glob supported)
|
||||
* --message=PATTERN Delete sessions whose first message contains PATTERN (case-insensitive)
|
||||
* --orphaned Delete sessions whose path no longer exists
|
||||
* --force Skip confirmation prompt
|
||||
* --help Show this help message
|
||||
@@ -21,6 +23,7 @@
|
||||
* bun run server/scripts/cleanup-sessions.ts
|
||||
* bun run server/scripts/cleanup-sessions.ts --min-messages=3
|
||||
* bun run server/scripts/cleanup-sessions.ts --path="/tmp/*"
|
||||
* bun run server/scripts/cleanup-sessions.ts --message="hello"
|
||||
* bun run server/scripts/cleanup-sessions.ts --orphaned
|
||||
* bun run server/scripts/cleanup-sessions.ts --orphaned --min-messages=5 --force
|
||||
*/
|
||||
@@ -30,11 +33,48 @@ import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { existsSync } from 'node:fs'
|
||||
|
||||
// Format timestamp as human-readable date
|
||||
function formatDate(timestamp: number): string {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Truncate string to max length with ellipsis
|
||||
function truncate(str: string, maxLen: number): string {
|
||||
if (str.length <= maxLen) return str
|
||||
return str.slice(0, maxLen - 3) + '...'
|
||||
}
|
||||
|
||||
// Extract text from user message content
|
||||
function extractUserText(content: unknown): string | null {
|
||||
if (!content || typeof content !== 'object') return null
|
||||
const c = content as Record<string, unknown>
|
||||
if (c.role !== 'user') return null
|
||||
const inner = c.content
|
||||
// Handle { content: { type: 'text', text: '...' } }
|
||||
if (inner && typeof inner === 'object') {
|
||||
const textObj = inner as Record<string, unknown>
|
||||
if (textObj.type === 'text' && typeof textObj.text === 'string') {
|
||||
return textObj.text
|
||||
}
|
||||
}
|
||||
// Handle { content: '...' } (string)
|
||||
if (typeof inner === 'string') {
|
||||
return inner
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
function parseArgs(): { minMessages: number | null; pathPattern: string | null; orphaned: boolean; force: boolean; help: boolean } {
|
||||
function parseArgs(): { minMessages: number | null; pathPattern: string | null; messagePattern: string | null; orphaned: boolean; force: boolean; help: boolean } {
|
||||
const args = process.argv.slice(2)
|
||||
let minMessages: number | null = null
|
||||
let pathPattern: string | null = null
|
||||
let messagePattern: string | null = null
|
||||
let orphaned = false
|
||||
let force = false
|
||||
let help = false
|
||||
@@ -55,6 +95,8 @@ function parseArgs(): { minMessages: number | null; pathPattern: string | null;
|
||||
minMessages = value
|
||||
} else if (arg.startsWith('--path=')) {
|
||||
pathPattern = arg.split('=').slice(1).join('=') // Handle paths with '='
|
||||
} else if (arg.startsWith('--message=')) {
|
||||
messagePattern = arg.split('=').slice(1).join('=').toLowerCase()
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`)
|
||||
console.error('Use --help for usage information')
|
||||
@@ -63,11 +105,11 @@ function parseArgs(): { minMessages: number | null; pathPattern: string | null;
|
||||
}
|
||||
|
||||
// Default behavior: if no filters specified, use min-messages=5
|
||||
if (minMessages === null && pathPattern === null && !orphaned) {
|
||||
if (minMessages === null && pathPattern === null && messagePattern === null && !orphaned) {
|
||||
minMessages = 5
|
||||
}
|
||||
|
||||
return { minMessages, pathPattern, orphaned, force, help }
|
||||
return { minMessages, pathPattern, messagePattern, orphaned, force, help }
|
||||
}
|
||||
|
||||
// Get database path (same logic as configuration.ts)
|
||||
@@ -84,41 +126,87 @@ function getDbPath(): string {
|
||||
// Session info for display
|
||||
interface SessionInfo {
|
||||
id: string
|
||||
tag: string | null
|
||||
title: string | null
|
||||
firstUserMessage: string | null
|
||||
path: string | null
|
||||
updatedAt: number
|
||||
messageCount: number
|
||||
}
|
||||
|
||||
// Query sessions with message counts
|
||||
function querySessions(db: Database): SessionInfo[] {
|
||||
const rows = db.query<
|
||||
{ id: string; tag: string | null; metadata: string | null; message_count: number },
|
||||
// Get basic session info
|
||||
const sessionRows = db.query<
|
||||
{ id: string; metadata: string | null; updated_at: number; message_count: number },
|
||||
[]
|
||||
>(`
|
||||
SELECT
|
||||
s.id,
|
||||
s.tag,
|
||||
s.metadata,
|
||||
s.updated_at,
|
||||
COUNT(m.id) as message_count
|
||||
FROM sessions s
|
||||
LEFT JOIN messages m ON m.session_id = s.id
|
||||
GROUP BY s.id
|
||||
`).all()
|
||||
|
||||
return rows.map(row => {
|
||||
// Get all messages for processing
|
||||
const messageRows = db.query<
|
||||
{ session_id: string; content: string; seq: number },
|
||||
[]
|
||||
>(`
|
||||
SELECT session_id, content, seq
|
||||
FROM messages
|
||||
ORDER BY session_id, seq
|
||||
`).all()
|
||||
|
||||
// Group messages by session
|
||||
const messagesBySession = new Map<string, { content: string; seq: number }[]>()
|
||||
for (const msg of messageRows) {
|
||||
const list = messagesBySession.get(msg.session_id) ?? []
|
||||
list.push({ content: msg.content, seq: msg.seq })
|
||||
messagesBySession.set(msg.session_id, list)
|
||||
}
|
||||
|
||||
return sessionRows.map(row => {
|
||||
let path: string | null = null
|
||||
let title: string | null = null
|
||||
if (row.metadata) {
|
||||
try {
|
||||
const metadata = JSON.parse(row.metadata)
|
||||
path = metadata.path ?? null
|
||||
// Get title from metadata.summary.text
|
||||
if (metadata.summary && typeof metadata.summary.text === 'string') {
|
||||
title = metadata.summary.text
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// Extract first user message from session messages
|
||||
let firstUserMessage: string | null = null
|
||||
const messages = messagesBySession.get(row.id) ?? []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (firstUserMessage !== null) break
|
||||
try {
|
||||
const content = JSON.parse(msg.content)
|
||||
const userText = extractUserText(content)
|
||||
if (userText) {
|
||||
firstUserMessage = userText
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
tag: row.tag,
|
||||
title,
|
||||
firstUserMessage,
|
||||
path,
|
||||
updatedAt: row.updated_at,
|
||||
messageCount: row.message_count,
|
||||
}
|
||||
})
|
||||
@@ -129,6 +217,7 @@ function filterSessions(
|
||||
sessions: SessionInfo[],
|
||||
minMessages: number | null,
|
||||
pathPattern: string | null,
|
||||
messagePattern: string | null,
|
||||
orphaned: boolean
|
||||
): SessionInfo[] {
|
||||
let filtered = sessions
|
||||
@@ -147,6 +236,14 @@ function filterSessions(
|
||||
})
|
||||
}
|
||||
|
||||
// Filter by first message pattern (case-insensitive fuzzy match)
|
||||
if (messagePattern !== null) {
|
||||
filtered = filtered.filter(s => {
|
||||
if (!s.firstUserMessage) return false
|
||||
return s.firstUserMessage.toLowerCase().includes(messagePattern)
|
||||
})
|
||||
}
|
||||
|
||||
// Filter by orphaned (path does not exist) if specified
|
||||
if (orphaned) {
|
||||
filtered = filtered.filter(s => {
|
||||
@@ -165,29 +262,37 @@ function displaySessions(sessions: SessionInfo[]): void {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate column widths
|
||||
const idWidth = Math.max(8, ...sessions.map(s => s.id.length))
|
||||
const tagWidth = Math.max(3, ...sessions.map(s => (s.tag ?? '').length))
|
||||
const pathWidth = Math.max(4, ...sessions.map(s => (s.path ?? '').length))
|
||||
const countWidth = 5
|
||||
// Fixed column widths for readability
|
||||
const dateWidth = 12
|
||||
const countWidth = 4
|
||||
const titleWidth = 25
|
||||
const messageWidth = 30
|
||||
const pathWidth = 30
|
||||
|
||||
// Header
|
||||
const header = [
|
||||
'ID'.padEnd(idWidth),
|
||||
'Tag'.padEnd(tagWidth),
|
||||
'Path'.padEnd(pathWidth),
|
||||
'Updated'.padEnd(dateWidth),
|
||||
'Msgs'.padStart(countWidth),
|
||||
'Title'.padEnd(titleWidth),
|
||||
'First Message'.padEnd(messageWidth),
|
||||
'Path'.padEnd(pathWidth),
|
||||
].join(' | ')
|
||||
console.log(header)
|
||||
console.log('-'.repeat(header.length))
|
||||
|
||||
// Rows
|
||||
for (const s of sessions) {
|
||||
const updated = formatDate(s.updatedAt)
|
||||
const title = truncate(s.title ?? '(no title)', titleWidth)
|
||||
const firstMsg = truncate(s.firstUserMessage ?? '(no message)', messageWidth)
|
||||
const path = truncate(s.path ?? '', pathWidth)
|
||||
|
||||
console.log([
|
||||
s.id.padEnd(idWidth),
|
||||
(s.tag ?? '').padEnd(tagWidth),
|
||||
(s.path ?? '').padEnd(pathWidth),
|
||||
updated.padEnd(dateWidth),
|
||||
s.messageCount.toString().padStart(countWidth),
|
||||
title.padEnd(titleWidth),
|
||||
firstMsg.padEnd(messageWidth),
|
||||
path.padEnd(pathWidth),
|
||||
].join(' | '))
|
||||
}
|
||||
}
|
||||
@@ -207,13 +312,13 @@ function deleteSessions(db: Database, ids: string[]): number {
|
||||
if (ids.length === 0) return 0
|
||||
|
||||
const placeholders = ids.map(() => '?').join(', ')
|
||||
const result = db.run(`DELETE FROM sessions WHERE id IN (${placeholders})`, ids)
|
||||
return result.changes
|
||||
db.run(`DELETE FROM sessions WHERE id IN (${placeholders})`, ids)
|
||||
return ids.length
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function main(): Promise<void> {
|
||||
const { minMessages, pathPattern, orphaned, force, help } = parseArgs()
|
||||
const { minMessages, pathPattern, messagePattern, orphaned, force, help } = parseArgs()
|
||||
|
||||
if (help) {
|
||||
console.log(`
|
||||
@@ -222,6 +327,7 @@ Usage: bun run server/scripts/cleanup-sessions.ts [options]
|
||||
Options:
|
||||
--min-messages=N Delete sessions with fewer than N messages (default: 5)
|
||||
--path=PATTERN Delete sessions matching path pattern (glob supported)
|
||||
--message=PATTERN Delete sessions whose first message contains PATTERN (case-insensitive)
|
||||
--orphaned Delete sessions whose path no longer exists
|
||||
--force Skip confirmation prompt
|
||||
--help Show this help message
|
||||
@@ -229,6 +335,7 @@ Options:
|
||||
Filtering logic:
|
||||
- Only --min-messages: Delete sessions with message count < N
|
||||
- Only --path: Delete ALL sessions matching the path pattern
|
||||
- Only --message: Delete sessions whose first user message contains the pattern
|
||||
- Only --orphaned: Delete sessions whose path does not exist on filesystem
|
||||
- Multiple filters: Delete sessions matching ALL conditions (AND)
|
||||
|
||||
@@ -236,6 +343,7 @@ Examples:
|
||||
bun run server/scripts/cleanup-sessions.ts
|
||||
bun run server/scripts/cleanup-sessions.ts --min-messages=3
|
||||
bun run server/scripts/cleanup-sessions.ts --path="/tmp/*"
|
||||
bun run server/scripts/cleanup-sessions.ts --message="hello"
|
||||
bun run server/scripts/cleanup-sessions.ts --orphaned
|
||||
bun run server/scripts/cleanup-sessions.ts --orphaned --min-messages=5 --force
|
||||
`)
|
||||
@@ -261,7 +369,7 @@ Examples:
|
||||
console.log(`Total sessions: ${allSessions.length}`)
|
||||
|
||||
// Apply filters
|
||||
const toDelete = filterSessions(allSessions, minMessages, pathPattern, orphaned)
|
||||
const toDelete = filterSessions(allSessions, minMessages, pathPattern, messagePattern, orphaned)
|
||||
|
||||
// Display filter criteria
|
||||
const criteria: string[] = []
|
||||
@@ -271,6 +379,9 @@ Examples:
|
||||
if (pathPattern !== null) {
|
||||
criteria.push(`path matches "${pathPattern}"`)
|
||||
}
|
||||
if (messagePattern !== null) {
|
||||
criteria.push(`first message contains "${messagePattern}"`)
|
||||
}
|
||||
if (orphaned) {
|
||||
criteria.push('path does not exist')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user