UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

262 lines (261 loc) 9.99 kB
import { r as isPathInside } from "./path-guards-Cp-mGr3-.js"; import { t as runTasksWithConcurrency } from "./run-with-concurrency-Dtu208ef.js"; import "./concurrency-runtime-kU4Hd9Jc.js"; import "./file-access-runtime-CfcyqU8y.js"; import { t as createDirtyDirectoryWatch } from "./session-catalog-tree-watch-CQi4yGOk.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region extensions/anthropic/session-catalog-scan.ts const CLAUDE_PARTIAL_SCAN_TTL_MS = 15e3; const CLAUDE_SESSION_SCAN_HARD_TTL_MS = 3e5; const MAX_CATALOG_JSON_CACHE_ENTRIES = 4e3; const CLAUDE_METADATA_WINDOW_BYTES = 1048576; const CLAUDE_METADATA_READ_CHUNK_BYTES = 16384; const CLAUDE_CATALOG_IO_CONCURRENCY = 32; async function readClaudeCatalogMetadata(handle, fileSize, maxBytes, inspectLine) { let pending = Buffer.alloc(0); let fileOffset = 0; let scannedBytes = 0; let stopDiscovery = false; let skipPartial = false; const readWindow = async (end, metadataOnly) => { while (fileOffset < end && scannedBytes < maxBytes) { const size = Math.min(CLAUDE_METADATA_READ_CHUNK_BYTES, end - fileOffset, maxBytes - scannedBytes); const chunk = Buffer.allocUnsafe(size); const { bytesRead } = await handle.read(chunk, 0, size, fileOffset); if (bytesRead === 0) return; fileOffset += bytesRead; scannedBytes += bytesRead; pending = pending.length ? Buffer.concat([pending, chunk.subarray(0, bytesRead)]) : chunk.subarray(0, bytesRead); let newline; while ((newline = pending.indexOf(10)) >= 0) { if (!skipPartial) stopDiscovery = inspectLine(pending.subarray(0, newline), metadataOnly || stopDiscovery) || stopDiscovery; skipPartial = false; pending = pending.subarray(newline + 1); } if (stopDiscovery && !metadataOnly) return; } }; await readWindow(Math.min(fileSize, CLAUDE_METADATA_WINDOW_BYTES), false); const prefixReadToEnd = fileOffset >= fileSize; const tailOffset = Math.max(fileOffset, fileSize - CLAUDE_METADATA_WINDOW_BYTES); skipPartial = tailOffset > fileOffset; if (skipPartial) { fileOffset = tailOffset - 1; pending = Buffer.alloc(0); } await readWindow(fileSize, true); if (fileOffset >= fileSize && !skipPartial && pending.length > 0) inspectLine(pending, stopDiscovery || !prefixReadToEnd); return { scannedBytes, complete: fileOffset >= fileSize }; } const projectTreeSlots = /* @__PURE__ */ new Map(); const catalogJsonCache = /* @__PURE__ */ new Map(); function setBoundedCache(cache, key, value, maxEntries, onEvict) { cache.delete(key); cache.set(key, value); while (cache.size > maxEntries) { const oldest = cache.entries().next(); if (oldest.done) break; onEvict?.(oldest.value[1]); cache.delete(oldest.value[0]); } } async function safeSessionFile(root, resolvedRoot, candidate, sessionId) { if (!isPathInside(root, candidate) || path.basename(candidate) !== `${sessionId}.jsonl`) return; try { const resolvedCandidate = await fs.realpath(candidate); if (!isPathInside(resolvedRoot, resolvedCandidate)) return; const stat = await fs.stat(resolvedCandidate); return stat.isFile() ? { filePath: resolvedCandidate, mtimeMs: stat.mtimeMs, size: stat.size, ino: stat.ino } : void 0; } catch (error) { const code = error && typeof error === "object" && "code" in error ? error.code : void 0; if (code === "ENOENT" || code === "ENOTDIR") return; throw new Error("Claude session file validation failed", { cause: error }); } } function safeSessionFileForScan(context, candidate, sessionId) { if (!context.resolvedRoot) return Promise.resolve(void 0); const resolved = path.resolve(candidate); const name = path.basename(resolved); const directory = context.directoriesByPath.get(path.dirname(resolved)); const signature = directory?.files.get(name); if (directory && signature && name === `${sessionId}.jsonl`) return Promise.resolve({ filePath: path.join(directory.resolvedDirectory, name), ...signature }); const key = `${sessionId}\0${resolved}`; let pending = context.safeFiles.get(key); if (!pending) { pending = safeSessionFile(context.root, context.resolvedRoot, candidate, sessionId).catch(() => { context.complete = false; if (context.safeFiles.get(key) === pending) context.safeFiles.delete(key); }); context.safeFiles.set(key, pending); } return pending; } async function readJsonFile(filePath, options = {}) { const stat = options.signature ?? await fs.stat(filePath).then((value) => value.isFile() ? value : void 0, () => { options.onIoFailure?.(); }); if (!stat) { catalogJsonCache.delete(filePath); return; } const cached = catalogJsonCache.get(filePath); if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size && cached.ino === stat.ino) { setBoundedCache(catalogJsonCache, filePath, cached, MAX_CATALOG_JSON_CACHE_ENTRIES); return cached.value; } let content; try { content = await fs.readFile(filePath, "utf8"); } catch { options.onIoFailure?.(); return; } try { const value = JSON.parse(content); setBoundedCache(catalogJsonCache, filePath, { mtimeMs: stat.mtimeMs, size: stat.size, ino: stat.ino, value }, MAX_CATALOG_JSON_CACHE_ENTRIES); return value; } catch { return; } } async function childDirectories(root) { try { return (await fs.readdir(root, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path.join(root, entry.name)); } catch { return []; } } function projectsDir(homeDir, configDir) { return path.join(configDir ?? path.join(homeDir, ".claude"), "projects"); } async function readProjectsTreeSnapshot(root, options = {}) { let slot = projectTreeSlots.get(root); if (slot?.pending) { if (!options.forceRefresh) return slot.pending; await slot.pending; return readProjectsTreeSnapshot(root, options); } if (!slot) slot = { watch: createDirtyDirectoryWatch(root), hardExpiresAt: 0 }; const current = slot; const previous = current.snapshot; const dirty = current.watch.takeDirty(); const full = !previous?.resolvedRoot || options.forceRefresh || current.hardExpiresAt <= Date.now() || dirty === "all"; setBoundedCache(projectTreeSlots, root, current, 8, (evicted) => evicted.watch.close()); if (!full && dirty.size === 0 && previous) return previous; current.pending = (async () => { let complete = true; const onReadFailure = () => { complete = false; }; const entries = full ? await fs.readdir(root, { withFileTypes: true }).catch(() => void 0) : void 0; const resolvedRoot = full ? await fs.realpath(root).catch(() => void 0) : previous?.resolvedRoot; if (!resolvedRoot || full && !entries) { current.watch.close(); if (projectTreeSlots.get(root) === current) projectTreeSlots.delete(root); return { root, projectDirectories: [], treeStamp: "unavailable" }; } const directories = new Map(full ? [] : previous?.projectDirectories.map((dir) => [dir.name, dir])); const names = entries ? entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name) : dirty === "all" ? [] : [...dirty]; current.watch.observeChildDirectories(/* @__PURE__ */ new Set([...directories.keys(), ...names])); const { results } = await runTasksWithConcurrency({ tasks: names.map((name) => async () => { const directory = path.join(root, name); const stat = await fs.lstat(directory).catch(onReadFailure); if (!stat?.isDirectory()) { directories.delete(name); return; } const childNames = await fs.readdir(directory).catch(onReadFailure) ?? []; return { name, directory, resolvedDirectory: path.join(resolvedRoot, name), childNames, mtimeMs: stat.mtimeMs, files: /* @__PURE__ */ new Map() }; }), limit: 32, throwOnError: true }); await runTasksWithConcurrency({ tasks: results.flatMap((dir) => dir ? dir.childNames.map((name) => async () => { const stat = await fs.lstat(path.join(dir.directory, name)).catch(onReadFailure); if (stat?.isFile()) dir.files.set(name, { mtimeMs: stat.mtimeMs, size: stat.size, ino: stat.ino }); }) : []), limit: 32, throwOnError: true }); for (const dir of results) if (dir) { const files = dir.childNames.map((name) => [name, dir.files.get(name)]); directories.set(dir.name, { ...dir, stamp: JSON.stringify([ dir.name, dir.mtimeMs, files ]) }); } const projectDirectories = [...directories.values()].toSorted((a, b) => a.name.localeCompare(b.name)); current.watch.observeChildDirectories(directories.keys()); if (full) current.hardExpiresAt = Date.now() + CLAUDE_SESSION_SCAN_HARD_TTL_MS; if (!complete) current.hardExpiresAt = Math.min(current.hardExpiresAt, Date.now() + CLAUDE_PARTIAL_SCAN_TTL_MS); return { root, resolvedRoot, projectDirectories, treeStamp: JSON.stringify([resolvedRoot, projectDirectories.map((dir) => dir.stamp)]) }; })().then((snapshot) => { current.snapshot = snapshot; return snapshot; }).finally(() => { current.pending = void 0; }); return current.pending; } function desktopSessionsDir(homeDir) { return path.join(homeDir, "Library", "Application Support", "Claude", "claude-code-sessions"); } function configuredClaudeConfigDir(env = process.env) { const configured = env.CLAUDE_CONFIG_DIR?.trim(); return configured ? path.resolve(configured) : void 0; } function gatewayClaudeScanOptions(allowProcessHomeFallback) { const configDir = configuredClaudeConfigDir(); return { ...configDir ? { configDir } : {}, includeDesktop: allowProcessHomeFallback !== false }; } //#endregion export { configuredClaudeConfigDir as a, projectsDir as c, readProjectsTreeSnapshot as d, safeSessionFileForScan as f, childDirectories as i, readClaudeCatalogMetadata as l, CLAUDE_PARTIAL_SCAN_TTL_MS as n, desktopSessionsDir as o, setBoundedCache as p, CLAUDE_SESSION_SCAN_HARD_TTL_MS as r, gatewayClaudeScanOptions as s, CLAUDE_CATALOG_IO_CONCURRENCY as t, readJsonFile as u };