UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

342 lines (341 loc) 13.8 kB
import { v as parseDateFirstTimestampMs } from "./number-coercion-CLj0HTDM.js"; import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { i as normalizeBoundedOptionalString } from "./string-coerce-CIXf7egm.js"; import { t as runTasksWithConcurrency } from "./run-with-concurrency-Dtu208ef.js"; import "./number-runtime-Cy4drVnh.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import "./concurrency-runtime-kU4Hd9Jc.js"; import { t as resolveClaudeCatalogHomeDir } from "./session-catalog-home-DC4c4SgQ.js"; import { c as projectsDir, d as readProjectsTreeSnapshot, f as safeSessionFileForScan, l as readClaudeCatalogMetadata, n as CLAUDE_PARTIAL_SCAN_TTL_MS, p as setBoundedCache, r as CLAUDE_SESSION_SCAN_HARD_TTL_MS, u as readJsonFile } from "./session-catalog-scan-4wxzSKEc.js"; import { a as readDesktopOverlay, n as desktopPullRequestSummary, r as emptyDesktopOverlay, t as MAX_STRING_LENGTH } from "./session-catalog-desktop-jTmHJBRJ.js"; import { t as collectTranscriptText } from "./session-catalog-transcript-C6d689--.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region extensions/anthropic/session-catalog-discovery.ts const MAX_CATALOG_DISCOVERY_FILES = 1e4; const MAX_CATALOG_DISCOVERY_CACHE_ENTRIES = 2e4; const MAX_CLAUDE_SESSION_SCAN_CACHE_ENTRIES = 8; const MAX_CATALOG_METADATA_SCAN_BYTES = 67108864; const CLI_ENTRYPOINTS = /* @__PURE__ */ new Set(["cli", "sdk-cli"]); const catalogDiscoveryCache = /* @__PURE__ */ new Map(); const claudeSessionScanCache = /* @__PURE__ */ new Map(); const mergedScans = /* @__PURE__ */ new WeakMap(); function cacheCatalogDiscovery(filePath, entry) { setBoundedCache(catalogDiscoveryCache, filePath, entry, MAX_CATALOG_DISCOVERY_CACHE_ENTRIES); } function applyCatalogDiscovery(records, sessionId, discovery) { const record = records.get(sessionId) ?? discovery.record; if (record) records.set(sessionId, { ...record, ...discovery.metadata, name: discovery.metadata.name ?? record.name ?? discovery.record?.name ?? null }); } function isCliEntrypoint(value) { return typeof value === "string" && CLI_ENTRYPOINTS.has(value); } function parseClaudeCatalogTimestampMs(value) { return parseDateFirstTimestampMs(value); } async function readIndexRecords(context) { const records = /* @__PURE__ */ new Map(); const sidechainIds = /* @__PURE__ */ new Set(); if (!context.resolvedRoot) return { records, sidechainIds }; const { results: indexes } = await runTasksWithConcurrency({ tasks: context.projectDirectories.map(({ directory, childNames, files }) => async () => ({ directory, raw: childNames.includes("sessions-index.json") ? await readJsonFile(path.join(directory, "sessions-index.json"), { signature: files.get("sessions-index.json"), onIoFailure: () => { context.complete = false; } }) : void 0 })), limit: 32, throwOnError: true }); for (const { directory, raw } of indexes) { if (!isRecord(raw) || !Array.isArray(raw.entries)) continue; for (const candidate of raw.entries) { if (!isRecord(candidate)) continue; const entry = candidate; const sessionId = normalizeBoundedOptionalString(entry.sessionId, 256); if (!sessionId) continue; if (entry.isSidechain === true) { sidechainIds.add(sessionId); records.delete(sessionId); continue; } const indexedPath = normalizeBoundedOptionalString(entry.fullPath, MAX_STRING_LENGTH); const safeFile = await safeSessionFileForScan(context, indexedPath ?? path.join(directory, `${sessionId}.jsonl`), sessionId); if (!safeFile) continue; const createdAt = parseClaudeCatalogTimestampMs(entry.created); const updatedAt = parseClaudeCatalogTimestampMs(entry.modified) ?? parseClaudeCatalogTimestampMs(entry.fileMtime); const summary = normalizeBoundedOptionalString(entry.summary, 500); const firstPrompt = normalizeBoundedOptionalString(entry.firstPrompt, 500); records.set(sessionId, { threadId: sessionId, name: summary ?? firstPrompt ?? null, cwd: normalizeBoundedOptionalString(entry.projectPath, MAX_STRING_LENGTH), status: "stored", ...createdAt !== void 0 ? { createdAt } : {}, ...updatedAt !== void 0 ? { updatedAt, recencyAt: updatedAt } : {}, source: "claude-cli", modelProvider: "anthropic", ...normalizeBoundedOptionalString(entry.gitBranch, 500) ? { gitBranch: normalizeBoundedOptionalString(entry.gitBranch, 500) } : {}, archived: false, filePath: safeFile.filePath }); } } return { records, sidechainIds }; } async function locateSessionFile(context, sessionId) { const fileName = `${sessionId}.jsonl`; for (const { directory, childNames } of context.projectDirectories) { if (!childNames.includes(fileName)) continue; const candidate = path.join(directory, fileName); const safeFile = await safeSessionFileForScan(context, candidate, sessionId); if (safeFile) return safeFile.filePath; } } async function discoverCliRecords(context, records, sidechainIds) { const { root } = context; if (!context.resolvedRoot) { for (const [cachedPath, entry] of catalogDiscoveryCache) if (entry.root === root) catalogDiscoveryCache.delete(cachedPath); return; } let discoveredFiles = 0; let scannedBytes = 0; let truncated = false; const seenFilePaths = /* @__PURE__ */ new Set(); const pendingIndexedFiles = new Set([...records.values()].map((record) => record.filePath)); const candidates = []; collect: for (const { directory, childNames } of context.projectDirectories) for (const name of childNames) { if (!name.endsWith(".jsonl")) continue; if (discoveredFiles >= MAX_CATALOG_DISCOVERY_FILES) { truncated = true; break collect; } discoveredFiles += 1; const sessionId = name.slice(0, -6); if (sessionId) candidates.push({ directory, name, sessionId }); } for (const { directory, name, sessionId } of candidates) { if (sidechainIds.has(sessionId)) continue; const fileStat = await safeSessionFileForScan(context, path.join(directory, name), sessionId); if (!fileStat) continue; const { filePath } = fileStat; if (records.has(sessionId) && !pendingIndexedFiles.delete(filePath)) continue; seenFilePaths.add(filePath); const cached = catalogDiscoveryCache.get(filePath); if (cached && cached.root === root && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size && cached.ino === fileStat.ino && cached.sessionId === sessionId && scannedBytes + cached.scannedBytes <= MAX_CATALOG_METADATA_SCAN_BYTES) { if (cached.sidechain) sidechainIds.add(sessionId); applyCatalogDiscovery(records, sessionId, cached); scannedBytes += cached.scannedBytes; if (scannedBytes >= MAX_CATALOG_METADATA_SCAN_BYTES) { truncated = true; break; } continue; } const handle = await fs.open(filePath, "r").catch(() => { context.complete = false; }); if (!handle) continue; let cacheable = false; let fileScannedBytes = 0; let record = null; let metadata = {}; try { const stat = await handle.stat(); let aiTitle; let customTitle; let color; const inspectLine = (line, metadataOnly) => { let raw; try { raw = JSON.parse(line.toString("utf8")); } catch { return false; } if (!isRecord(raw) || raw.sessionId !== sessionId) return false; if (raw.type === "ai-title") { aiTitle = normalizeBoundedOptionalString(raw.aiTitle, 500); return false; } if (raw.type === "custom-title") { customTitle = normalizeBoundedOptionalString(raw.customTitle, 500); return false; } if (raw.type === "agent-color") { color = normalizeBoundedOptionalString(raw.agentColor, MAX_STRING_LENGTH); return false; } if (metadataOnly) return false; if (typeof raw.entrypoint === "string" && !isCliEntrypoint(raw.entrypoint)) return true; if (isCliEntrypoint(raw.entrypoint) && raw.isSidechain === true) { sidechainIds.add(sessionId); return true; } if (!isCliEntrypoint(raw.entrypoint) || raw.type !== "user" || raw.isMeta === true || !isRecord(raw.message) || raw.message.role !== "user") return false; const fragments = []; collectTranscriptText(raw.message.content, fragments); const firstPrompt = normalizeBoundedOptionalString(fragments[0], 500); const createdAt = parseClaudeCatalogTimestampMs(raw.timestamp); record = { threadId: sessionId, name: firstPrompt ?? null, cwd: normalizeBoundedOptionalString(raw.cwd, MAX_STRING_LENGTH), status: "stored", ...createdAt !== void 0 ? { createdAt } : {}, updatedAt: stat.mtimeMs, recencyAt: stat.mtimeMs, source: "claude-cli", modelProvider: "anthropic", ...normalizeBoundedOptionalString(raw.version, 256) ? { cliVersion: normalizeBoundedOptionalString(raw.version, 256) } : {}, ...normalizeBoundedOptionalString(raw.gitBranch, 500) ? { gitBranch: normalizeBoundedOptionalString(raw.gitBranch, 500) } : {}, archived: false, filePath }; return true; }; const scan = await readClaudeCatalogMetadata(handle, stat.size, MAX_CATALOG_METADATA_SCAN_BYTES - scannedBytes, inspectLine); fileScannedBytes = scan.scannedBytes; scannedBytes += fileScannedBytes; metadata = { name: customTitle ?? aiTitle, color }; applyCatalogDiscovery(records, sessionId, { record, metadata }); cacheable = !(scannedBytes >= MAX_CATALOG_METADATA_SCAN_BYTES) && scan.complete; } finally { await handle.close(); } if (cacheable) cacheCatalogDiscovery(filePath, { root, mtimeMs: fileStat.mtimeMs, size: fileStat.size, ino: fileStat.ino, sessionId, scannedBytes: fileScannedBytes, record, metadata, sidechain: sidechainIds.has(sessionId) }); if (scannedBytes >= MAX_CATALOG_METADATA_SCAN_BYTES) { truncated = true; break; } } if (!truncated) { for (const [cachedPath, entry] of catalogDiscoveryCache) if (entry.root === root && !seenFilePaths.has(cachedPath)) catalogDiscoveryCache.delete(cachedPath); } } async function scanClaudeSessions(snapshot) { const context = { ...snapshot, complete: true, safeFiles: /* @__PURE__ */ new Map(), directoriesByPath: new Map(snapshot.projectDirectories.map((dir) => [dir.directory, dir])) }; const indexed = await readIndexRecords(context); await discoverCliRecords(context, indexed.records, indexed.sidechainIds); return { ...indexed, context }; } async function mergeClaudeSessions(cli, desktop) { const { context, sidechainIds } = cli; const records = new Map(cli.records); for (const sessionId of desktop.archived) records.delete(sessionId); for (const [sessionId, metadata] of desktop.active) { if (sidechainIds.has(sessionId)) continue; const existing = records.get(sessionId); const filePath = existing?.filePath ?? await locateSessionFile(context, sessionId); if (!filePath) continue; const createdAt = parseClaudeCatalogTimestampMs(metadata.createdAt) ?? existing?.createdAt; const updatedAt = parseClaudeCatalogTimestampMs(metadata.lastActivityAt) ?? existing?.updatedAt; const customGroup = normalizeBoundedOptionalString(metadata.customGroup, 500); const pullRequest = desktopPullRequestSummary(metadata); records.set(sessionId, { ...existing ?? { threadId: sessionId, status: "stored", modelProvider: "anthropic", archived: false }, name: normalizeBoundedOptionalString(metadata.title, 500) ?? existing?.name ?? null, cwd: normalizeBoundedOptionalString(metadata.cwd, 4096) ?? normalizeBoundedOptionalString(metadata.originCwd, 4096) ?? existing?.cwd, ...createdAt !== void 0 ? { createdAt } : {}, ...updatedAt !== void 0 ? { updatedAt, recencyAt: updatedAt } : {}, ...customGroup ? { customGroup } : {}, ...pullRequest ? { pullRequest } : {}, source: "claude-desktop", color: void 0, filePath }); } return [...records.values()].toSorted((left, right) => { return (right.recencyAt ?? right.updatedAt ?? 0) - (left.recencyAt ?? left.updatedAt ?? 0) || left.threadId.localeCompare(right.threadId); }); } async function readCliScan(treeSnapshot, forceRefresh) { const cacheKey = `${treeSnapshot.root}\0cli`; const now = Date.now(); const cached = claudeSessionScanCache.get(cacheKey); if (!forceRefresh && cached?.treeStamp === treeSnapshot.treeStamp && cached.hardExpiresAt > now) { setBoundedCache(claudeSessionScanCache, cacheKey, cached, MAX_CLAUDE_SESSION_SCAN_CACHE_ENTRIES); return cached.records; } const entry = { treeStamp: treeSnapshot.treeStamp, hardExpiresAt: now + CLAUDE_SESSION_SCAN_HARD_TTL_MS, records: scanClaudeSessions(treeSnapshot) }; setBoundedCache(claudeSessionScanCache, cacheKey, entry, MAX_CLAUDE_SESSION_SCAN_CACHE_ENTRIES); try { const result = await entry.records; if (!result.context.complete) entry.hardExpiresAt = Date.now() + CLAUDE_PARTIAL_SCAN_TTL_MS; return result; } catch (error) { if (claudeSessionScanCache.get(cacheKey) === entry) claudeSessionScanCache.delete(cacheKey); throw error; } } async function listClaudeSessions(homeDir = resolveClaudeCatalogHomeDir(), options = {}) { const [cli, desktop] = await Promise.all([readProjectsTreeSnapshot(projectsDir(homeDir, options.configDir), options).then((snapshot) => readCliScan(snapshot, options.forceRefresh)), options.includeDesktop !== false ? readDesktopOverlay(homeDir, options.forceRefresh) : emptyDesktopOverlay]); let overlays = mergedScans.get(cli); if (!overlays) { overlays = /* @__PURE__ */ new WeakMap(); mergedScans.set(cli, overlays); } let merged = overlays.get(desktop); if (!merged) { merged = mergeClaudeSessions(cli, desktop); overlays.set(desktop, merged); } return merged; } //#endregion export { listClaudeSessions as t };