/** * 从 Claude projects 目录扫描已 /rename(jsonl 含 custom-title)且尚未登记到 * managed store 的会话,供「导入」候选列表使用。 */ import { promises as fsp } from 'node:fs' import * as path from 'node:path' import { projectsDirFor } from '../cc/sessionWatcher' import { readManagedSessions } from './managedStore' export interface ImportableNamedSession { id: string name: string mtimeMs: number } /** 从 jsonl 文本提取最后一条 custom-title(trim 后非空),否则 null */ export function extractLastCustomTitle(jsonlText: string): string | null { let last: string | null = null const lines = jsonlText.split(/\r?\n/) for (const line of lines) { const trimmed = line.trim() if (!trimmed) continue let parsed: unknown try { parsed = JSON.parse(trimmed) } catch { continue } if (!parsed || typeof parsed !== 'object') continue const rec = parsed as { type?: unknown, customTitle?: unknown } if (rec.type !== 'custom-title') continue if (typeof rec.customTitle !== 'string') continue const title = rec.customTitle.trim() if (!title) continue last = title } return last } /** * 列出可导入命名会话:扫 projectsDir 顶层 *.jsonl,过滤已在 managed 的 id,mtime 降序。 * `projectsDir` 可选,便于测试注入;默认 `projectsDirFor(workspaceRoot)`。 */ export async function listImportableNamedSessions( workspaceRoot: string, projectsDir?: string, ): Promise { const dir = projectsDir ?? projectsDirFor(workspaceRoot) let entries: import('node:fs').Dirent[] try { entries = await fsp.readdir(dir, { withFileTypes: true }) } catch { return [] } const managed = await readManagedSessions(workspaceRoot) const managedIds = new Set(managed.sessions.map(s => s.id)) const results: ImportableNamedSession[] = [] for (const ent of entries) { if (!ent.isFile() || !ent.name.endsWith('.jsonl')) continue const id = ent.name.slice(0, -'.jsonl'.length) if (!id || managedIds.has(id)) continue const filePath = path.join(dir, ent.name) let text: string let mtimeMs: number try { const [raw, st] = await Promise.all([ fsp.readFile(filePath, 'utf8'), fsp.stat(filePath), ]) text = raw mtimeMs = st.mtimeMs } catch { continue } const name = extractLastCustomTitle(text) if (!name) continue results.push({ id, name, mtimeMs }) } results.sort((a, b) => b.mtimeMs - a.mtimeMs) return results }