UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

869 lines (865 loc) 34.1 kB
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js"; import { g as resolveStateDir } from "./paths-B4BZAPZh.js"; import { B as theme, P as setVerbose, R as colorize, S as shortenHomePath, x as shortenHomeInString, y as resolveUserPath, z as isRich } from "./utils-CP9YLh6M.js"; import { f as defaultRuntime, t as createSubsystemLogger } from "./subsystem-BCQGGxdd.js"; import { c as resolveAgentWorkspaceDir, l as resolveDefaultAgentId } from "./agent-scope-BnZW9Gh2.js"; import { C as parseDurationMs, i as loadConfig } from "./config-PQiujvsf.js"; import { n as formatErrorMessage } from "./errors-kfGqPQ4b.js"; import { s as resolveSessionTranscriptsDirForAgent } from "./paths-C2NfoGZE.js"; import { d as listMemoryFiles, f as normalizeExtraMemoryPaths } from "./sqlite-JcMMx8Z5.js"; import { t as splitShellArgs } from "./shell-argv-ChJujo6D.js"; import { t as formatDocsLink } from "./links-BVCMOGeE.js"; import { r as withManager } from "./cli-utils-CX1oQ81G.js"; import { t as formatHelpExamples } from "./help-format-dbk2xW0E.js"; import { n as withProgress, r as withProgressTotals } from "./progress-CaVMHLaE.js"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import fs$1 from "node:fs/promises"; //#region src/memory/backend-config.ts const DEFAULT_BACKEND = "builtin"; const DEFAULT_CITATIONS = "auto"; const DEFAULT_QMD_INTERVAL = "5m"; const DEFAULT_QMD_DEBOUNCE_MS = 15e3; const DEFAULT_QMD_TIMEOUT_MS = 4e3; const DEFAULT_QMD_SEARCH_MODE = "search"; const DEFAULT_QMD_EMBED_INTERVAL = "60m"; const DEFAULT_QMD_COMMAND_TIMEOUT_MS = 3e4; const DEFAULT_QMD_UPDATE_TIMEOUT_MS = 12e4; const DEFAULT_QMD_EMBED_TIMEOUT_MS = 12e4; const DEFAULT_QMD_LIMITS = { maxResults: 6, maxSnippetChars: 700, maxInjectedChars: 4e3, timeoutMs: DEFAULT_QMD_TIMEOUT_MS }; const DEFAULT_QMD_SCOPE = { default: "deny", rules: [{ action: "allow", match: { chatType: "direct" } }] }; function sanitizeName(input) { return input.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "collection"; } function scopeCollectionBase(base, agentId) { return `${base}-${sanitizeName(agentId)}`; } function ensureUniqueName(base, existing) { let name = sanitizeName(base); if (!existing.has(name)) { existing.add(name); return name; } let suffix = 2; while (existing.has(`${name}-${suffix}`)) suffix += 1; const unique = `${name}-${suffix}`; existing.add(unique); return unique; } function resolvePath(raw, workspaceDir) { const trimmed = raw.trim(); if (!trimmed) throw new Error("path required"); if (trimmed.startsWith("~") || path.isAbsolute(trimmed)) return path.normalize(resolveUserPath(trimmed)); return path.normalize(path.resolve(workspaceDir, trimmed)); } function resolveIntervalMs(raw) { const value = raw?.trim(); if (!value) return parseDurationMs(DEFAULT_QMD_INTERVAL, { defaultUnit: "m" }); try { return parseDurationMs(value, { defaultUnit: "m" }); } catch { return parseDurationMs(DEFAULT_QMD_INTERVAL, { defaultUnit: "m" }); } } function resolveEmbedIntervalMs(raw) { const value = raw?.trim(); if (!value) return parseDurationMs(DEFAULT_QMD_EMBED_INTERVAL, { defaultUnit: "m" }); try { return parseDurationMs(value, { defaultUnit: "m" }); } catch { return parseDurationMs(DEFAULT_QMD_EMBED_INTERVAL, { defaultUnit: "m" }); } } function resolveDebounceMs(raw) { if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) return Math.floor(raw); return DEFAULT_QMD_DEBOUNCE_MS; } function resolveTimeoutMs(raw, fallback) { if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.floor(raw); return fallback; } function resolveLimits(raw) { const parsed = { ...DEFAULT_QMD_LIMITS }; if (raw?.maxResults && raw.maxResults > 0) parsed.maxResults = Math.floor(raw.maxResults); if (raw?.maxSnippetChars && raw.maxSnippetChars > 0) parsed.maxSnippetChars = Math.floor(raw.maxSnippetChars); if (raw?.maxInjectedChars && raw.maxInjectedChars > 0) parsed.maxInjectedChars = Math.floor(raw.maxInjectedChars); if (raw?.timeoutMs && raw.timeoutMs > 0) parsed.timeoutMs = Math.floor(raw.timeoutMs); return parsed; } function resolveSearchMode(raw) { if (raw === "search" || raw === "vsearch" || raw === "query") return raw; return DEFAULT_QMD_SEARCH_MODE; } function resolveSessionConfig(cfg, workspaceDir) { const enabled = Boolean(cfg?.enabled); const exportDirRaw = cfg?.exportDir?.trim(); return { enabled, exportDir: exportDirRaw ? resolvePath(exportDirRaw, workspaceDir) : void 0, retentionDays: cfg?.retentionDays && cfg.retentionDays > 0 ? Math.floor(cfg.retentionDays) : void 0 }; } function resolveCustomPaths(rawPaths, workspaceDir, existing, agentId) { if (!rawPaths?.length) return []; const collections = []; rawPaths.forEach((entry, index) => { const trimmedPath = entry?.path?.trim(); if (!trimmedPath) return; let resolved; try { resolved = resolvePath(trimmedPath, workspaceDir); } catch { return; } const pattern = entry.pattern?.trim() || "**/*.md"; const name = ensureUniqueName(scopeCollectionBase(entry.name?.trim() || `custom-${index + 1}`, agentId), existing); collections.push({ name, path: resolved, pattern, kind: "custom" }); }); return collections; } function resolveDefaultCollections(include, workspaceDir, existing, agentId) { if (!include) return []; return [ { path: workspaceDir, pattern: "MEMORY.md", base: "memory-root" }, { path: workspaceDir, pattern: "memory.md", base: "memory-alt" }, { path: path.join(workspaceDir, "memory"), pattern: "**/*.md", base: "memory-dir" } ].map((entry) => ({ name: ensureUniqueName(scopeCollectionBase(entry.base, agentId), existing), path: entry.path, pattern: entry.pattern, kind: "memory" })); } function resolveMemoryBackendConfig(params) { const backend = params.cfg.memory?.backend ?? DEFAULT_BACKEND; const citations = params.cfg.memory?.citations ?? DEFAULT_CITATIONS; if (backend !== "qmd") return { backend: "builtin", citations }; const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId); const qmdCfg = params.cfg.memory?.qmd; const includeDefaultMemory = qmdCfg?.includeDefaultMemory !== false; const nameSet = /* @__PURE__ */ new Set(); const collections = [...resolveDefaultCollections(includeDefaultMemory, workspaceDir, nameSet, params.agentId), ...resolveCustomPaths(qmdCfg?.paths, workspaceDir, nameSet, params.agentId)]; const rawCommand = qmdCfg?.command?.trim() || "qmd"; return { backend: "qmd", citations, qmd: { command: splitShellArgs(rawCommand)?.[0] || rawCommand.split(/\s+/)[0] || "qmd", searchMode: resolveSearchMode(qmdCfg?.searchMode), collections, includeDefaultMemory, sessions: resolveSessionConfig(qmdCfg?.sessions, workspaceDir), update: { intervalMs: resolveIntervalMs(qmdCfg?.update?.interval), debounceMs: resolveDebounceMs(qmdCfg?.update?.debounceMs), onBoot: qmdCfg?.update?.onBoot !== false, waitForBootSync: qmdCfg?.update?.waitForBootSync === true, embedIntervalMs: resolveEmbedIntervalMs(qmdCfg?.update?.embedInterval), commandTimeoutMs: resolveTimeoutMs(qmdCfg?.update?.commandTimeoutMs, DEFAULT_QMD_COMMAND_TIMEOUT_MS), updateTimeoutMs: resolveTimeoutMs(qmdCfg?.update?.updateTimeoutMs, DEFAULT_QMD_UPDATE_TIMEOUT_MS), embedTimeoutMs: resolveTimeoutMs(qmdCfg?.update?.embedTimeoutMs, DEFAULT_QMD_EMBED_TIMEOUT_MS) }, limits: resolveLimits(qmdCfg?.limits), scope: qmdCfg?.scope ?? DEFAULT_QMD_SCOPE } }; } //#endregion //#region src/memory/search-manager.ts const log = createSubsystemLogger("memory"); const QMD_MANAGER_CACHE = /* @__PURE__ */ new Map(); async function getMemorySearchManager(params) { const resolved = resolveMemoryBackendConfig(params); if (resolved.backend === "qmd" && resolved.qmd) { const statusOnly = params.purpose === "status"; const cacheKey = buildQmdCacheKey(params.agentId, resolved.qmd); if (!statusOnly) { const cached = QMD_MANAGER_CACHE.get(cacheKey); if (cached) return { manager: cached }; } try { const { QmdMemoryManager } = await import("./qmd-manager-DFAJZz7F.js"); const primary = await QmdMemoryManager.create({ cfg: params.cfg, agentId: params.agentId, resolved, mode: statusOnly ? "status" : "full" }); if (primary) { if (statusOnly) return { manager: primary }; const wrapper = new FallbackMemoryManager({ primary, fallbackFactory: async () => { const { MemoryIndexManager } = await import("./manager-C0teWocQ.js").then((n) => n.t); return await MemoryIndexManager.get(params); } }, () => QMD_MANAGER_CACHE.delete(cacheKey)); QMD_MANAGER_CACHE.set(cacheKey, wrapper); return { manager: wrapper }; } } catch (err) { const message = err instanceof Error ? err.message : String(err); log.warn(`qmd memory unavailable; falling back to builtin: ${message}`); } } try { const { MemoryIndexManager } = await import("./manager-C0teWocQ.js").then((n) => n.t); return { manager: await MemoryIndexManager.get(params) }; } catch (err) { return { manager: null, error: err instanceof Error ? err.message : String(err) }; } } var FallbackMemoryManager = class { constructor(deps, onClose) { this.deps = deps; this.onClose = onClose; this.fallback = null; this.primaryFailed = false; this.cacheEvicted = false; } async search(query, opts) { if (!this.primaryFailed) try { return await this.deps.primary.search(query, opts); } catch (err) { this.primaryFailed = true; this.lastError = err instanceof Error ? err.message : String(err); log.warn(`qmd memory failed; switching to builtin index: ${this.lastError}`); await this.deps.primary.close?.().catch(() => {}); this.evictCacheEntry(); } const fallback = await this.ensureFallback(); if (fallback) return await fallback.search(query, opts); throw new Error(this.lastError ?? "memory search unavailable"); } async readFile(params) { if (!this.primaryFailed) return await this.deps.primary.readFile(params); const fallback = await this.ensureFallback(); if (fallback) return await fallback.readFile(params); throw new Error(this.lastError ?? "memory read unavailable"); } status() { if (!this.primaryFailed) return this.deps.primary.status(); const fallbackStatus = this.fallback?.status(); const fallbackInfo = { from: "qmd", reason: this.lastError ?? "unknown" }; if (fallbackStatus) { const custom = fallbackStatus.custom ?? {}; return { ...fallbackStatus, fallback: fallbackInfo, custom: { ...custom, fallback: { disabled: true, reason: this.lastError ?? "unknown" } } }; } const primaryStatus = this.deps.primary.status(); const custom = primaryStatus.custom ?? {}; return { ...primaryStatus, fallback: fallbackInfo, custom: { ...custom, fallback: { disabled: true, reason: this.lastError ?? "unknown" } } }; } async sync(params) { if (!this.primaryFailed) { await this.deps.primary.sync?.(params); return; } await (await this.ensureFallback())?.sync?.(params); } async probeEmbeddingAvailability() { if (!this.primaryFailed) return await this.deps.primary.probeEmbeddingAvailability(); const fallback = await this.ensureFallback(); if (fallback) return await fallback.probeEmbeddingAvailability(); return { ok: false, error: this.lastError ?? "memory embeddings unavailable" }; } async probeVectorAvailability() { if (!this.primaryFailed) return await this.deps.primary.probeVectorAvailability(); return await (await this.ensureFallback())?.probeVectorAvailability() ?? false; } async close() { await this.deps.primary.close?.(); await this.fallback?.close?.(); this.evictCacheEntry(); } async ensureFallback() { if (this.fallback) return this.fallback; let fallback; try { fallback = await this.deps.fallbackFactory(); if (!fallback) { log.warn("memory fallback requested but builtin index is unavailable"); return null; } } catch (err) { const message = err instanceof Error ? err.message : String(err); log.warn(`memory fallback unavailable: ${message}`); return null; } this.fallback = fallback; return this.fallback; } evictCacheEntry() { if (this.cacheEvicted) return; this.cacheEvicted = true; this.onClose?.(); } }; function buildQmdCacheKey(agentId, config) { return `${agentId}:${stableSerialize(config)}`; } function stableSerialize(value) { return JSON.stringify(sortValue(value)); } function sortValue(value) { if (Array.isArray(value)) return value.map((entry) => sortValue(entry)); if (value && typeof value === "object") { const sortedEntries = Object.keys(value).toSorted((a, b) => a.localeCompare(b)).map((key) => [key, sortValue(value[key])]); return Object.fromEntries(sortedEntries); } return value; } //#endregion //#region src/cli/memory-cli.ts var memory_cli_exports = /* @__PURE__ */ __exportAll({ registerMemoryCli: () => registerMemoryCli, runMemoryStatus: () => runMemoryStatus }); function formatSourceLabel(source, workspaceDir, agentId) { if (source === "memory") return shortenHomeInString(`memory (MEMORY.md + ${path.join(workspaceDir, "memory")}${path.sep}*.md)`); if (source === "sessions") { const stateDir = resolveStateDir(process.env, os.homedir); return shortenHomeInString(`sessions (${path.join(stateDir, "agents", agentId, "sessions")}${path.sep}*.jsonl)`); } return source; } function resolveAgent(cfg, agent) { const trimmed = agent?.trim(); if (trimmed) return trimmed; return resolveDefaultAgentId(cfg); } function resolveAgentIds(cfg, agent) { const trimmed = agent?.trim(); if (trimmed) return [trimmed]; const list = cfg.agents?.list ?? []; if (list.length > 0) return list.map((entry) => entry.id).filter(Boolean); return [resolveDefaultAgentId(cfg)]; } function formatExtraPaths(workspaceDir, extraPaths) { return normalizeExtraMemoryPaths(workspaceDir, extraPaths).map((entry) => shortenHomePath(entry)); } async function withMemoryManagerForAgent(params) { const managerParams = { cfg: params.cfg, agentId: params.agentId }; if (params.purpose) managerParams.purpose = params.purpose; await withManager({ getManager: () => getMemorySearchManager(managerParams), onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."), onCloseError: (err) => defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`), close: async (manager) => { await manager.close?.(); }, run: params.run }); } async function checkReadableFile(pathname) { try { await fs$1.access(pathname, fs.constants.R_OK); return { exists: true }; } catch (err) { const code = err.code; if (code === "ENOENT") return { exists: false }; return { exists: true, issue: `${shortenHomePath(pathname)} not readable (${code ?? "error"})` }; } } async function scanSessionFiles(agentId) { const issues = []; const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId); try { return { source: "sessions", totalFiles: (await fs$1.readdir(sessionsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).length, issues }; } catch (err) { const code = err.code; if (code === "ENOENT") { issues.push(`sessions directory missing (${shortenHomePath(sessionsDir)})`); return { source: "sessions", totalFiles: 0, issues }; } issues.push(`sessions directory not accessible (${shortenHomePath(sessionsDir)}): ${code ?? "error"}`); return { source: "sessions", totalFiles: null, issues }; } } async function scanMemoryFiles(workspaceDir, extraPaths = []) { const issues = []; const memoryFile = path.join(workspaceDir, "MEMORY.md"); const altMemoryFile = path.join(workspaceDir, "memory.md"); const memoryDir = path.join(workspaceDir, "memory"); const primary = await checkReadableFile(memoryFile); const alt = await checkReadableFile(altMemoryFile); if (primary.issue) issues.push(primary.issue); if (alt.issue) issues.push(alt.issue); const resolvedExtraPaths = normalizeExtraMemoryPaths(workspaceDir, extraPaths); for (const extraPath of resolvedExtraPaths) try { if ((await fs$1.lstat(extraPath)).isSymbolicLink()) continue; const extraCheck = await checkReadableFile(extraPath); if (extraCheck.issue) issues.push(extraCheck.issue); } catch (err) { const code = err.code; if (code === "ENOENT") issues.push(`additional memory path missing (${shortenHomePath(extraPath)})`); else issues.push(`additional memory path not accessible (${shortenHomePath(extraPath)}): ${code ?? "error"}`); } let dirReadable = null; try { await fs$1.access(memoryDir, fs.constants.R_OK); dirReadable = true; } catch (err) { const code = err.code; if (code === "ENOENT") { issues.push(`memory directory missing (${shortenHomePath(memoryDir)})`); dirReadable = false; } else { issues.push(`memory directory not accessible (${shortenHomePath(memoryDir)}): ${code ?? "error"}`); dirReadable = null; } } let listed = []; let listedOk = false; try { listed = await listMemoryFiles(workspaceDir, resolvedExtraPaths); listedOk = true; } catch (err) { const code = err.code; if (dirReadable !== null) { issues.push(`memory directory scan failed (${shortenHomePath(memoryDir)}): ${code ?? "error"}`); dirReadable = null; } } let totalFiles = 0; if (dirReadable === null) totalFiles = null; else { const files = new Set(listedOk ? listed : []); if (!listedOk) { if (primary.exists) files.add(memoryFile); if (alt.exists) files.add(altMemoryFile); } totalFiles = files.size; } if ((totalFiles ?? 0) === 0 && issues.length === 0) issues.push(`no memory files found in ${shortenHomePath(workspaceDir)}`); return { source: "memory", totalFiles, issues }; } async function summarizeQmdIndexArtifact(manager) { const status = manager.status?.(); if (!status || status.backend !== "qmd") return null; const dbPath = status.dbPath?.trim(); if (!dbPath) return null; let stat; try { stat = await fs$1.stat(dbPath); } catch (err) { const code = err.code; if (code === "ENOENT") throw new Error(`QMD index file not found: ${shortenHomePath(dbPath)}`, { cause: err }); throw new Error(`QMD index file check failed: ${shortenHomePath(dbPath)} (${code ?? "error"})`, { cause: err }); } if (!stat.isFile() || stat.size <= 0) throw new Error(`QMD index file is empty: ${shortenHomePath(dbPath)}`); return `QMD index: ${shortenHomePath(dbPath)} (${stat.size} bytes)`; } async function scanMemorySources(params) { const scans = []; const extraPaths = params.extraPaths ?? []; for (const source of params.sources) { if (source === "memory") scans.push(await scanMemoryFiles(params.workspaceDir, extraPaths)); if (source === "sessions") scans.push(await scanSessionFiles(params.agentId)); } const issues = scans.flatMap((scan) => scan.issues); const totals = scans.map((scan) => scan.totalFiles); const numericTotals = totals.filter((total) => total !== null); return { sources: scans, totalFiles: totals.some((total) => total === null) ? null : numericTotals.reduce((sum, total) => sum + total, 0), issues }; } async function runMemoryStatus(opts) { setVerbose(Boolean(opts.verbose)); const cfg = loadConfig(); const agentIds = resolveAgentIds(cfg, opts.agent); const allResults = []; for (const agentId of agentIds) await withMemoryManagerForAgent({ cfg, agentId, purpose: opts.index ? "default" : "status", run: async (manager) => { const deep = Boolean(opts.deep || opts.index); let embeddingProbe; let indexError; const syncFn = manager.sync ? manager.sync.bind(manager) : void 0; if (deep) { await withProgress({ label: "Checking memory…", total: 2 }, async (progress) => { progress.setLabel("Probing vector…"); await manager.probeVectorAvailability(); progress.tick(); progress.setLabel("Probing embeddings…"); embeddingProbe = await manager.probeEmbeddingAvailability(); progress.tick(); }); if (opts.index && syncFn) await withProgressTotals({ label: "Indexing memory…", total: 0, fallback: opts.verbose ? "line" : void 0 }, async (update, progress) => { try { await syncFn({ reason: "cli", force: Boolean(opts.force), progress: (syncUpdate) => { update({ completed: syncUpdate.completed, total: syncUpdate.total, label: syncUpdate.label }); if (syncUpdate.label) progress.setLabel(syncUpdate.label); } }); } catch (err) { indexError = formatErrorMessage(err); defaultRuntime.error(`Memory index failed: ${indexError}`); process.exitCode = 1; } }); else if (opts.index && !syncFn) defaultRuntime.log("Memory backend does not support manual reindex."); } else await manager.probeVectorAvailability(); const status = manager.status(); const sources = status.sources?.length ? status.sources : ["memory"]; const workspaceDir = status.workspaceDir; const scan = workspaceDir ? await scanMemorySources({ workspaceDir, agentId, sources, extraPaths: status.extraPaths }) : void 0; allResults.push({ agentId, status, embeddingProbe, indexError, scan }); } }); if (opts.json) { defaultRuntime.log(JSON.stringify(allResults, null, 2)); return; } const rich = isRich(); const heading = (text) => colorize(rich, theme.heading, text); const muted = (text) => colorize(rich, theme.muted, text); const info = (text) => colorize(rich, theme.info, text); const success = (text) => colorize(rich, theme.success, text); const warn = (text) => colorize(rich, theme.warn, text); const accent = (text) => colorize(rich, theme.accent, text); const label = (text) => muted(`${text}:`); for (const result of allResults) { const { agentId, status, embeddingProbe, indexError, scan } = result; const filesIndexed = status.files ?? 0; const chunksIndexed = status.chunks ?? 0; const totalFiles = scan?.totalFiles ?? null; const indexedLabel = totalFiles === null ? `${filesIndexed}/? files · ${chunksIndexed} chunks` : `${filesIndexed}/${totalFiles} files · ${chunksIndexed} chunks`; if (opts.index) { const line = indexError ? `Memory index failed: ${indexError}` : "Memory index complete."; defaultRuntime.log(line); } const requestedProvider = status.requestedProvider ?? status.provider; const modelLabel = status.model ?? status.provider; const storePath = status.dbPath ? shortenHomePath(status.dbPath) : "<unknown>"; const workspacePath = status.workspaceDir ? shortenHomePath(status.workspaceDir) : "<unknown>"; const sourceList = status.sources?.length ? status.sources.join(", ") : null; const extraPaths = status.workspaceDir ? formatExtraPaths(status.workspaceDir, status.extraPaths ?? []) : []; const lines = [ `${heading("Memory Search")} ${muted(`(${agentId})`)}`, `${label("Provider")} ${info(status.provider)} ${muted(`(requested: ${requestedProvider})`)}`, `${label("Model")} ${info(modelLabel)}`, sourceList ? `${label("Sources")} ${info(sourceList)}` : null, extraPaths.length ? `${label("Extra paths")} ${info(extraPaths.join(", "))}` : null, `${label("Indexed")} ${success(indexedLabel)}`, `${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`, `${label("Store")} ${info(storePath)}`, `${label("Workspace")} ${info(workspacePath)}` ].filter(Boolean); if (embeddingProbe) { const state = embeddingProbe.ok ? "ready" : "unavailable"; const stateColor = embeddingProbe.ok ? theme.success : theme.warn; lines.push(`${label("Embeddings")} ${colorize(rich, stateColor, state)}`); if (embeddingProbe.error) lines.push(`${label("Embeddings error")} ${warn(embeddingProbe.error)}`); } if (status.sourceCounts?.length) { lines.push(label("By source")); for (const entry of status.sourceCounts) { const total = scan?.sources?.find((scanEntry) => scanEntry.source === entry.source)?.totalFiles; const counts = total === null ? `${entry.files}/? files · ${entry.chunks} chunks` : `${entry.files}/${total} files · ${entry.chunks} chunks`; lines.push(` ${accent(entry.source)} ${muted("·")} ${muted(counts)}`); } } if (status.fallback) lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`); if (status.vector) { const vectorState = status.vector.enabled ? status.vector.available === void 0 ? "unknown" : status.vector.available ? "ready" : "unavailable" : "disabled"; const vectorColor = vectorState === "ready" ? theme.success : vectorState === "unavailable" ? theme.warn : theme.muted; lines.push(`${label("Vector")} ${colorize(rich, vectorColor, vectorState)}`); if (status.vector.dims) lines.push(`${label("Vector dims")} ${info(String(status.vector.dims))}`); if (status.vector.extensionPath) lines.push(`${label("Vector path")} ${info(shortenHomePath(status.vector.extensionPath))}`); if (status.vector.loadError) lines.push(`${label("Vector error")} ${warn(status.vector.loadError)}`); } if (status.fts) { const ftsState = status.fts.enabled ? status.fts.available ? "ready" : "unavailable" : "disabled"; const ftsColor = ftsState === "ready" ? theme.success : ftsState === "unavailable" ? theme.warn : theme.muted; lines.push(`${label("FTS")} ${colorize(rich, ftsColor, ftsState)}`); if (status.fts.error) lines.push(`${label("FTS error")} ${warn(status.fts.error)}`); } if (status.cache) { const cacheState = status.cache.enabled ? "enabled" : "disabled"; const cacheColor = status.cache.enabled ? theme.success : theme.muted; const suffix = status.cache.enabled && typeof status.cache.entries === "number" ? ` (${status.cache.entries} entries)` : ""; lines.push(`${label("Embedding cache")} ${colorize(rich, cacheColor, cacheState)}${suffix}`); if (status.cache.enabled && typeof status.cache.maxEntries === "number") lines.push(`${label("Cache cap")} ${info(String(status.cache.maxEntries))}`); } if (status.batch) { const batchState = status.batch.enabled ? "enabled" : "disabled"; const batchColor = status.batch.enabled ? theme.success : theme.warn; const batchSuffix = ` (failures ${status.batch.failures}/${status.batch.limit})`; lines.push(`${label("Batch")} ${colorize(rich, batchColor, batchState)}${muted(batchSuffix)}`); if (status.batch.lastError) lines.push(`${label("Batch error")} ${warn(status.batch.lastError)}`); } if (status.fallback?.reason) lines.push(muted(status.fallback.reason)); if (indexError) lines.push(`${label("Index error")} ${warn(indexError)}`); if (scan?.issues.length) { lines.push(label("Issues")); for (const issue of scan.issues) lines.push(` ${warn(issue)}`); } defaultRuntime.log(lines.join("\n")); defaultRuntime.log(""); } } function registerMemoryCli(program) { const memory = program.command("memory").description("Search, inspect, and reindex memory files").addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([ ["openclaw memory status", "Show index and provider status."], ["openclaw memory index --force", "Force a full reindex."], ["openclaw memory search --query \"deployment notes\"", "Search indexed memory entries."], ["openclaw memory status --json", "Output machine-readable JSON."] ])}\n\n${theme.muted("Docs:")} ${formatDocsLink("/cli/memory", "docs.openclaw.ai/cli/memory")}\n`); memory.command("status").description("Show memory search index status").option("--agent <id>", "Agent id (default: default agent)").option("--json", "Print JSON").option("--deep", "Probe embedding provider availability").option("--index", "Reindex if dirty (implies --deep)").option("--verbose", "Verbose logging", false).action(async (opts) => { await runMemoryStatus(opts); }); memory.command("index").description("Reindex memory files").option("--agent <id>", "Agent id (default: default agent)").option("--force", "Force full reindex", false).option("--verbose", "Verbose logging", false).action(async (opts) => { setVerbose(Boolean(opts.verbose)); const cfg = loadConfig(); const agentIds = resolveAgentIds(cfg, opts.agent); for (const agentId of agentIds) await withMemoryManagerForAgent({ cfg, agentId, run: async (manager) => { try { const syncFn = manager.sync ? manager.sync.bind(manager) : void 0; if (opts.verbose) { const status = manager.status(); const rich = isRich(); const heading = (text) => colorize(rich, theme.heading, text); const muted = (text) => colorize(rich, theme.muted, text); const info = (text) => colorize(rich, theme.info, text); const warn = (text) => colorize(rich, theme.warn, text); const label = (text) => muted(`${text}:`); const sourceLabels = (status.sources ?? []).map((source) => formatSourceLabel(source, status.workspaceDir ?? "", agentId)); const extraPaths = status.workspaceDir ? formatExtraPaths(status.workspaceDir, status.extraPaths ?? []) : []; const requestedProvider = status.requestedProvider ?? status.provider; const modelLabel = status.model ?? status.provider; const lines = [ `${heading("Memory Index")} ${muted(`(${agentId})`)}`, `${label("Provider")} ${info(status.provider)} ${muted(`(requested: ${requestedProvider})`)}`, `${label("Model")} ${info(modelLabel)}`, sourceLabels.length ? `${label("Sources")} ${info(sourceLabels.join(", "))}` : null, extraPaths.length ? `${label("Extra paths")} ${info(extraPaths.join(", "))}` : null ].filter(Boolean); if (status.fallback) lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`); defaultRuntime.log(lines.join("\n")); defaultRuntime.log(""); } const startedAt = Date.now(); let lastLabel = "Indexing memory…"; let lastCompleted = 0; let lastTotal = 0; const formatElapsed = () => { const elapsedMs = Math.max(0, Date.now() - startedAt); const seconds = Math.floor(elapsedMs / 1e3); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`; }; const formatEta = () => { if (lastTotal <= 0 || lastCompleted <= 0) return null; const elapsedMs = Math.max(1, Date.now() - startedAt); const rate = lastCompleted / elapsedMs; if (!Number.isFinite(rate) || rate <= 0) return null; const remainingMs = Math.max(0, (lastTotal - lastCompleted) / rate); const seconds = Math.floor(remainingMs / 1e3); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`; }; const buildLabel = () => { const elapsed = formatElapsed(); const eta = formatEta(); return eta ? `${lastLabel} · elapsed ${elapsed} · eta ${eta}` : `${lastLabel} · elapsed ${elapsed}`; }; if (!syncFn) { defaultRuntime.log("Memory backend does not support manual reindex."); return; } await withProgressTotals({ label: "Indexing memory…", total: 0, fallback: opts.verbose ? "line" : void 0 }, async (update, progress) => { const interval = setInterval(() => { progress.setLabel(buildLabel()); }, 1e3); try { await syncFn({ reason: "cli", force: Boolean(opts.force), progress: (syncUpdate) => { if (syncUpdate.label) lastLabel = syncUpdate.label; lastCompleted = syncUpdate.completed; lastTotal = syncUpdate.total; update({ completed: syncUpdate.completed, total: syncUpdate.total, label: buildLabel() }); progress.setLabel(buildLabel()); } }); } finally { clearInterval(interval); } }); const qmdIndexSummary = await summarizeQmdIndexArtifact(manager); if (qmdIndexSummary) defaultRuntime.log(qmdIndexSummary); defaultRuntime.log(`Memory index updated (${agentId}).`); } catch (err) { const message = formatErrorMessage(err); defaultRuntime.error(`Memory index failed (${agentId}): ${message}`); process.exitCode = 1; } } }); }); memory.command("search").description("Search memory files").argument("<query>", "Search query").option("--agent <id>", "Agent id (default: default agent)").option("--max-results <n>", "Max results", (value) => Number(value)).option("--min-score <n>", "Minimum score", (value) => Number(value)).option("--json", "Print JSON").action(async (query, opts) => { const cfg = loadConfig(); await withMemoryManagerForAgent({ cfg, agentId: resolveAgent(cfg, opts.agent), run: async (manager) => { let results; try { results = await manager.search(query, { maxResults: opts.maxResults, minScore: opts.minScore }); } catch (err) { const message = formatErrorMessage(err); defaultRuntime.error(`Memory search failed: ${message}`); process.exitCode = 1; return; } if (opts.json) { defaultRuntime.log(JSON.stringify({ results }, null, 2)); return; } if (results.length === 0) { defaultRuntime.log("No matches."); return; } const rich = isRich(); const lines = []; for (const result of results) { lines.push(`${colorize(rich, theme.success, result.score.toFixed(3))} ${colorize(rich, theme.accent, `${shortenHomePath(result.path)}:${result.startLine}-${result.endLine}`)}`); lines.push(colorize(rich, theme.muted, result.snippet)); lines.push(""); } defaultRuntime.log(lines.join("\n").trim()); } }); }); } //#endregion export { resolveMemoryBackendConfig as i, registerMemoryCli as n, getMemorySearchManager as r, memory_cli_exports as t };