add hermes kanban board (#534)

This commit is contained in:
ekko
2026-05-08 11:32:47 +08:00
committed by GitHub
parent 9fbff08098
commit b0e03ae838
26 changed files with 3467 additions and 0 deletions
@@ -859,6 +859,107 @@ export async function listSessionSummaries(source?: string, limit = 2000, profil
}
}
export async function searchSessionSummariesWithProfile(
query: string,
profile: string,
source?: string,
limit = 20,
): Promise<HermesSessionSearchRow[]> {
if (!SQLITE_AVAILABLE) {
throw new Error(`node:sqlite requires Node >= 22.5, current: ${process.versions.node}`)
}
const trimmed = query.trim()
if (!trimmed) return []
const { DatabaseSync } = await import('node:sqlite')
const dbPath = `${getProfileDir(profile)}/state.db`
const db = new DatabaseSync(dbPath, { open: true, readOnly: true })
const normalized = sanitizeFtsQuery(trimmed)
const prefixQuery = toPrefixQuery(normalized)
const titlePattern = buildLikePattern(normalizeTitleLikeQuery(trimmed).toLowerCase())
const useLiteralContentSearch = containsCjk(trimmed) || shouldUseLiteralContentSearch(trimmed)
const candidateLimit = searchCandidateLimit(limit)
try {
const sourceClause = source ? 'AND s.source = ?' : ''
const sourceParams = source ? [source] : []
const titleSql = `
WITH base AS (
SELECT
${SESSION_SELECT},
s.parent_session_id AS parent_session_id
FROM sessions s
WHERE s.source != 'tool' AND s.id NOT LIKE 'compress_%'
${sourceClause}
)
SELECT
base.*,
NULL AS matched_message_id,
CASE
WHEN base.title IS NOT NULL AND base.title != '' THEN base.title
ELSE base.preview
END AS snippet,
0 AS rank
FROM base
WHERE LOWER(COALESCE(base.title, '')) LIKE ? ESCAPE '\\'
ORDER BY base.last_active DESC
LIMIT ?
`
const titleRows = db.prepare(titleSql).all(...sourceParams, titlePattern, candidateLimit) as Record<string, unknown>[]
const contentSql = `
WITH base AS (
SELECT
${SESSION_SELECT},
s.parent_session_id AS parent_session_id
FROM sessions s
WHERE s.source != 'tool' AND s.id NOT LIKE 'compress_%'
${sourceClause}
)
SELECT
base.*,
m.id AS matched_message_id,
snippet(messages_fts, 0, '>>>', '<<<', '...', 40) AS snippet,
bm25(messages_fts) AS rank
FROM messages_fts
JOIN messages m ON m.id = messages_fts.rowid
JOIN base ON base.id = m.session_id
WHERE messages_fts MATCH ?
ORDER BY rank, base.last_active DESC
LIMIT ?
`
const contentRows = useLiteralContentSearch
? runLiteralContentSearch(db, source, trimmed, candidateLimit)
: prefixQuery
? (db.prepare(contentSql).all(...sourceParams, prefixQuery, candidateLimit) as Record<string, unknown>[])
: []
const idx = loadAllSessions(db)
const merged = new Map<string, HermesSessionSearchRow>()
for (const row of titleRows) {
const mapped = projectSearchRow(row, idx, source)
if (mapped) merged.set(mapped.id, mapped)
}
for (const row of contentRows) {
const mapped = projectSearchRow(row, idx, source)
if (mapped && !merged.has(mapped.id)) merged.set(mapped.id, mapped)
}
const items = [...merged.values()]
items.sort((a, b) => {
if (a.rank !== b.rank) return a.rank - b.rank
return b.last_active - a.last_active
})
return items.slice(0, limit)
} catch (_err) {
return []
} finally {
db.close()
}
}
export async function searchSessionSummaries(
query: string,
source?: string,