UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

300 lines (299 loc) 13.5 kB
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; import { y as resolveStateDir } from "./paths-mvMm5bYV.js"; import { p as tryReadJsonSync } from "./json-files-2umMHm0W.js"; import { a as resolveDefaultPluginNpmDir, d as validatePluginId, l as resolvePluginNpmProjectsDir } from "./install-paths-D-sCLR1I.js"; import { c as resolveOpenClawStateSqlitePath, i as openOpenClawStateDatabase } from "./openclaw-state-db-ULtH_b7x.js"; import fs from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; //#region src/plugins/installed-plugin-index-record-cache.ts const installRecordsCache = /* @__PURE__ */ new Map(); let installRecordsCacheGeneration = 0; /** Returns cached installed plugin records for a store/recovery key. */ function getInstalledPluginIndexInstallRecordsCache(key) { return installRecordsCache.get(key); } /** Stores cached installed plugin records for a store/recovery key. */ function setInstalledPluginIndexInstallRecordsCache(key, entry) { installRecordsCache.set(key, entry); } /** Current cache generation used to detect concurrent clears during async loads. */ function getInstalledPluginIndexInstallRecordsCacheGeneration() { return installRecordsCacheGeneration; } /** Clears cached installed plugin records and advances the cache generation. */ function clearLoadInstalledPluginIndexInstallRecordsCache() { installRecordsCacheGeneration += 1; installRecordsCache.clear(); } //#endregion //#region src/plugins/installed-plugin-index-store-path.ts const LEGACY_INSTALLED_PLUGIN_INDEX_STORE_PATH = path.join("plugins", "installs.json"); function resolveStoreEnv(options) { return options.stateDir ? { ...options.env ?? process.env, OPENCLAW_STATE_DIR: options.stateDir } : options.env ?? process.env; } /** Resolves the canonical SQLite-backed installed plugin index path. */ function resolveInstalledPluginIndexStorePath(options = {}) { if (options.filePath) return options.filePath; return resolveOpenClawStateSqlitePath(resolveStoreEnv(options)); } /** Resolves the legacy JSON installed plugin index path for migration/doctor use. */ function resolveLegacyInstalledPluginIndexStorePath(options = {}) { if (options.filePath) return options.filePath; const env = options.env ?? process.env; const stateDir = options.stateDir ?? resolveStateDir(env); return path.join(stateDir, LEGACY_INSTALLED_PLUGIN_INDEX_STORE_PATH); } //#endregion //#region src/plugins/npm-project-roots.ts function isMissing(error) { return error.code === "ENOENT"; } function sortPaths(paths) { return paths.toSorted((left, right) => left.localeCompare(right)); } /** Lists project-level npm roots managed below the plugin npm root. */ function listManagedPluginNpmProjectRootsSync(npmRoot) { const projectsDir = resolvePluginNpmProjectsDir(npmRoot); try { return sortPaths(fs.readdirSync(projectsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(projectsDir, entry.name))); } catch (error) { if (isMissing(error)) return []; throw error; } } /** Async variant of project-level managed npm root discovery. */ async function listManagedPluginNpmProjectRoots(npmRoot) { const projectsDir = resolvePluginNpmProjectsDir(npmRoot); try { return sortPaths((await fs$1.readdir(projectsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => path.join(projectsDir, entry.name))); } catch (error) { if (isMissing(error)) return []; throw error; } } /** Returns the root npm install plus all managed project npm roots. */ function listManagedPluginNpmRootsSync(npmRoot) { return [npmRoot, ...listManagedPluginNpmProjectRootsSync(npmRoot)]; } /** Async variant of managed npm root discovery. */ async function listManagedPluginNpmRoots(npmRoot) { return [npmRoot, ...await listManagedPluginNpmProjectRoots(npmRoot)]; } //#endregion //#region src/plugins/installed-plugin-index-record-reader.ts /** Reads installed-index records back into manifest registry records. */ function cloneInstallRecords(records) { return readRecordMap(records) ?? {}; } const BLOCKED_RECORD_KEYS = new Set([ "__proto__", "constructor", "prototype" ]); function isSafeRecordKey(key) { return !BLOCKED_RECORD_KEYS.has(key); } function readRecordMap(value) { if (!isRecord(value)) return null; const records = {}; for (const [pluginId, record] of Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right))) { if (!isSafeRecordKey(pluginId)) continue; if (isRecord(record) && typeof record.source === "string") records[pluginId] = structuredClone(record); } return records; } function readJsonObjectFileSync(filePath) { const parsed = tryReadJsonSync(filePath); return isRecord(parsed) ? parsed : null; } function readStringRecord(value) { if (!isRecord(value)) return {}; const record = {}; for (const [key, raw] of Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right))) { if (!isSafeRecordKey(key)) continue; if (typeof raw === "string" && raw.trim()) record[key] = raw.trim(); } return record; } function hasPackagePluginMetadata(manifest) { const openclaw = manifest.openclaw; if (!isRecord(openclaw)) return false; const extensions = openclaw.extensions; return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string"); } function readManifestPluginId(packageDir) { const manifest = readJsonObjectFileSync(path.join(packageDir, "openclaw.plugin.json")); return (typeof manifest?.id === "string" ? manifest.id.trim() : "") || void 0; } function resolveRecoveredManagedNpmRoot(options = {}) { return path.resolve(options.stateDir ? path.join(options.stateDir, "npm") : resolveDefaultPluginNpmDir(options.env)); } function resolveRecoveredManagedNpmPluginId(params) { const packageManifest = readJsonObjectFileSync(path.join(params.packageDir, "package.json")); if (!packageManifest || !hasPackagePluginMetadata(packageManifest)) return; const packageName = typeof packageManifest.name === "string" && packageManifest.name.trim() ? packageManifest.name.trim() : params.packageName; const pluginId = readManifestPluginId(params.packageDir) ?? packageName; return validatePluginId(pluginId) ? void 0 : pluginId; } function buildRecoveredManagedNpmInstallRecordsForRoot(npmRoot) { const dependencies = readStringRecord(readJsonObjectFileSync(path.join(npmRoot, "package.json"))?.dependencies); const records = {}; for (const [packageName, dependencySpec] of Object.entries(dependencies)) { const packageDir = path.join(npmRoot, "node_modules", ...packageName.split("/")); let stat; try { stat = fs.statSync(packageDir); } catch { continue; } if (!stat.isDirectory()) continue; const pluginId = resolveRecoveredManagedNpmPluginId({ packageName, packageDir }); if (!pluginId) continue; const packageManifest = readJsonObjectFileSync(path.join(packageDir, "package.json")); const version = typeof packageManifest?.version === "string" && packageManifest.version.trim() ? packageManifest.version.trim() : void 0; records[pluginId] = { source: "npm", spec: `${packageName}@${dependencySpec}`, installPath: packageDir, ...version ? { version, resolvedName: packageName, resolvedVersion: version } : {}, ...version ? { resolvedSpec: `${packageName}@${version}` } : {} }; } return records; } function buildRecoveredManagedNpmInstallRecords(options = {}) { const npmRoot = resolveRecoveredManagedNpmRoot(options); const legacyRecords = buildRecoveredManagedNpmInstallRecordsForRoot(npmRoot); const projectRecords = {}; for (const projectRoot of listManagedPluginNpmProjectRootsSync(npmRoot)) Object.assign(projectRecords, buildRecoveredManagedNpmInstallRecordsForRoot(projectRoot)); return { ...legacyRecords, ...projectRecords }; } function recordsShareInstallPath(left, right) { if (!left?.installPath || !right.installPath) return false; return path.resolve(left.installPath) === path.resolve(right.installPath); } function readInstallRecordVersion(record) { return record?.resolvedVersion ?? record?.version; } function mergeRecoveredManagedNpmRecord(params) { const persistedVersion = readInstallRecordVersion(params.persisted); const recoveredVersion = readInstallRecordVersion(params.recovered); if (params.persisted?.source === "npm" && recordsShareInstallPath(params.persisted, params.recovered) && recoveredVersion && persistedVersion !== recoveredVersion) { const next = { ...params.persisted, ...params.recovered }; delete next.integrity; delete next.shasum; delete next.resolvedAt; delete next.installedAt; return next; } return params.persisted ?? params.recovered; } function mergeRecoveredManagedNpmInstallRecords(persisted, options) { const recovered = buildRecoveredManagedNpmInstallRecords(options); const merged = { ...persisted }; for (const [pluginId, record] of Object.entries(recovered)) merged[pluginId] = mergeRecoveredManagedNpmRecord({ persisted: merged[pluginId], recovered: record }); return merged; } function extractPluginInstallRecordsFromPersistedInstalledPluginIndex(index) { if (!isRecord(index)) return null; if (Object.hasOwn(index, "installRecords")) return readRecordMap(index.installRecords) ?? {}; if (Object.hasOwn(index, "records")) return readRecordMap(index.records) ?? {}; if (!Array.isArray(index.plugins)) return null; const records = {}; for (const entry of index.plugins) { if (!isRecord(entry) || typeof entry.pluginId !== "string" || !isRecord(entry.installRecord)) continue; if (!isSafeRecordKey(entry.pluginId)) continue; records[entry.pluginId] = structuredClone(entry.installRecord); } return records; } function resolveStateDatabaseOptions(options = {}) { if (options.filePath) return { ...options.env ? { env: options.env } : {}, path: options.filePath }; if (options.stateDir) return { env: { ...options.env ?? process.env, OPENCLAW_STATE_DIR: options.stateDir } }; return options.env ? { env: options.env } : {}; } function parseJsonColumn(value) { try { return JSON.parse(value); } catch { return; } } function readPersistedInstalledPluginIndexForRecords(options = {}) { const storePath = resolveInstalledPluginIndexStorePath(options); if (!fs.existsSync(storePath)) return null; if (options.filePath?.endsWith(".json")) return tryReadJsonSync(options.filePath); try { const row = openOpenClawStateDatabase(resolveStateDatabaseOptions(options)).db.prepare(` SELECT install_records_json, plugins_json FROM installed_plugin_index WHERE index_key = ? `).get("installed-plugin-index"); if (!row) return null; return { installRecords: parseJsonColumn(row.install_records_json), plugins: parseJsonColumn(row.plugins_json) }; } catch { return null; } } /** Reads install records from the persisted installed plugin index. */ async function readPersistedInstalledPluginIndexInstallRecords(options = {}) { return extractPluginInstallRecordsFromPersistedInstalledPluginIndex(readPersistedInstalledPluginIndexForRecords(options)); } /** Synchronously reads install records from the persisted installed plugin index. */ function readPersistedInstalledPluginIndexInstallRecordsSync(options = {}) { return extractPluginInstallRecordsFromPersistedInstalledPluginIndex(readPersistedInstalledPluginIndexForRecords(options)); } function resolveInstallRecordsCacheKey(options) { return [path.resolve(resolveInstalledPluginIndexStorePath(options)), resolveRecoveredManagedNpmRoot(options)].join("\0"); } /** Loads installed plugin records, recovering managed npm installs and caching the result. */ async function loadInstalledPluginIndexInstallRecords(params = {}) { const cacheKey = resolveInstallRecordsCacheKey(params); const cached = getInstalledPluginIndexInstallRecordsCache(cacheKey); if (cached) return cloneInstallRecords(cached.records); const cacheGeneration = getInstalledPluginIndexInstallRecordsCacheGeneration(); const records = cloneInstallRecords(mergeRecoveredManagedNpmInstallRecords(await readPersistedInstalledPluginIndexInstallRecords(params), params)); if (cacheGeneration !== getInstalledPluginIndexInstallRecordsCacheGeneration()) return await loadInstalledPluginIndexInstallRecords(params); setInstalledPluginIndexInstallRecordsCache(cacheKey, { records }); return cloneInstallRecords(records); } /** Synchronously loads installed plugin records, recovering managed npm installs and caching them. */ function loadInstalledPluginIndexInstallRecordsSync(params = {}) { const cacheKey = resolveInstallRecordsCacheKey(params); const cached = getInstalledPluginIndexInstallRecordsCache(cacheKey); if (cached) return cloneInstallRecords(cached.records); const records = cloneInstallRecords(mergeRecoveredManagedNpmInstallRecords(readPersistedInstalledPluginIndexInstallRecordsSync(params), params)); setInstalledPluginIndexInstallRecordsCache(cacheKey, { records }); return cloneInstallRecords(records); } //#endregion export { listManagedPluginNpmRoots as a, resolveLegacyInstalledPluginIndexStorePath as c, readPersistedInstalledPluginIndexInstallRecordsSync as i, clearLoadInstalledPluginIndexInstallRecordsCache as l, loadInstalledPluginIndexInstallRecordsSync as n, listManagedPluginNpmRootsSync as o, readPersistedInstalledPluginIndexInstallRecords as r, resolveInstalledPluginIndexStorePath as s, loadInstalledPluginIndexInstallRecords as t };