From a4aad1c76ad2bbcc963d0526fce91ee7ae5ff35b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 15:29:43 +0000 Subject: [PATCH] feat(cli): expand memory status across agents Co-authored-by: Gustavo Madeira Santana --- docs/cli/memory.md | 19 +- src/cli/memory-cli.ts | 518 ++++++++++++++++++++---------------------- 2 files changed, 255 insertions(+), 282 deletions(-) diff --git a/docs/cli/memory.md b/docs/cli/memory.md index 1479af1e3..256fda9e5 100644 --- a/docs/cli/memory.md +++ b/docs/cli/memory.md @@ -7,8 +7,8 @@ read_when: # `clawdbot memory` -Memory search tools (semantic memory status/index/search). -Provided by the active memory plugin (default: `memory-core`; use `plugins.slots.memory = "none"` to disable). +Manage semantic memory indexing and search. +Provided by the active memory plugin (default: `memory-core`; set `plugins.slots.memory = "none"` to disable). Related: - Memory concept: [Memory](/concepts/memory) @@ -22,11 +22,20 @@ clawdbot memory status --deep clawdbot memory status --deep --index clawdbot memory status --deep --index --verbose clawdbot memory index +clawdbot memory index --verbose clawdbot memory search "release checklist" +clawdbot memory status --agent main +clawdbot memory index --agent main --verbose ``` ## Options -- `--verbose`: emit debug logs during memory probes and indexing. -- `--index-mode auto|batch|direct`: override batch usage when indexing (`direct` favors speed; `batch` favors OpenAI Batch pricing). -- `--progress auto|line|log|none`: progress output mode (`log` prints updates even without a TTY). +Common: + +- `--agent `: scope to a single agent (default: all configured agents). +- `--verbose`: emit detailed logs during probes and indexing. + +Notes: +- `memory status --deep` probes vector + embedding availability. +- `memory status --deep --index` runs a reindex if the store is dirty. +- `memory index --verbose` prints per-phase details (provider, model, sources, batch activity). diff --git a/src/cli/memory-cli.ts b/src/cli/memory-cli.ts index 5a9212781..0265ab315 100644 --- a/src/cli/memory-cli.ts +++ b/src/cli/memory-cli.ts @@ -1,36 +1,39 @@ +import os from "node:os"; +import path from "node:path"; + import type { Command } from "commander"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; -import type { ClawdbotConfig } from "../config/config.js"; -import type { MemorySearchConfig } from "../config/types.tools.js"; import { loadConfig } from "../config/config.js"; import { setVerbose } from "../globals.js"; import { withProgress, withProgressTotals } from "./progress.js"; import { formatErrorMessage, withManager } from "./cli-utils.js"; import { getMemorySearchManager, type MemorySearchManagerResult } from "../memory/index.js"; -import { - resolveMemoryCacheState, - resolveMemoryFtsState, - resolveMemoryVectorState, - type Tone, -} from "../memory/status-format.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { colorize, isRich, theme } from "../terminal/theme.js"; +import { resolveStateDir } from "../config/paths.js"; type MemoryCommandOptions = { agent?: string; json?: boolean; deep?: boolean; index?: boolean; - indexMode?: IndexMode; - progress?: ProgressMode; verbose?: boolean; }; type MemoryManager = NonNullable; -type IndexMode = "auto" | "batch" | "direct"; -type ProgressMode = "auto" | "line" | "log" | "none"; + +function formatSourceLabel(source: string, workspaceDir: string, agentId: string): string { + if (source === "memory") { + return `memory (MEMORY.md + ${path.join(workspaceDir, "memory")}${path.sep}*.md)`; + } + if (source === "sessions") { + const stateDir = resolveStateDir(process.env, os.homedir); + return `sessions (${path.join(stateDir, "agents", agentId, "sessions")}${path.sep}*.jsonl)`; + } + return source; +} function resolveAgent(cfg: ReturnType, agent?: string) { const trimmed = agent?.trim(); @@ -38,66 +41,14 @@ function resolveAgent(cfg: ReturnType, agent?: string) { return resolveDefaultAgentId(cfg); } -function resolveIndexMode(raw?: string): IndexMode { - if (!raw) return "auto"; - const trimmed = raw.trim().toLowerCase(); - if (trimmed === "batch") return "batch"; - if (trimmed === "direct") return "direct"; - return "auto"; -} - -function resolveProgressMode(raw?: string): ProgressMode { - if (!raw) return "auto"; - const trimmed = raw.trim().toLowerCase(); - if (trimmed === "line") return "line"; - if (trimmed === "log") return "log"; - if (trimmed === "none") return "none"; - return "auto"; -} - -function applyIndexMode(cfg: ClawdbotConfig, agentId: string, mode: IndexMode): ClawdbotConfig { - if (mode === "auto") return cfg; - const enabled = mode === "batch"; - const patchMemorySearch = (memorySearch?: MemorySearchConfig) => { - const remote = memorySearch?.remote; - const batch = remote?.batch; - return { - ...memorySearch, - remote: { - ...remote, - batch: { - ...batch, - enabled, - }, - }, - }; - }; - const nextAgents = { ...cfg.agents }; - nextAgents.defaults = { - ...cfg.agents?.defaults, - memorySearch: patchMemorySearch(cfg.agents?.defaults?.memorySearch), - }; - if (cfg.agents?.list?.length) { - nextAgents.list = cfg.agents.list.map((agent) => - agent.id === agentId - ? { - ...agent, - memorySearch: patchMemorySearch(agent.memorySearch), - } - : agent, - ); +function resolveAgentIds(cfg: ReturnType, agent?: string): string[] { + 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 { ...cfg, agents: nextAgents }; -} - -function resolveProgressOptions( - mode: ProgressMode, - verbose: boolean, -): { enabled?: boolean; fallback?: "spinner" | "line" | "log" | "none" } { - if (mode === "none") return { enabled: false, fallback: "none" }; - if (mode === "line") return { fallback: "line" }; - if (mode === "log") return { fallback: "log" }; - return { fallback: verbose ? "line" : undefined }; + return [resolveDefaultAgentId(cfg)]; } export function registerMemoryCli(program: Command) { @@ -117,182 +68,194 @@ export function registerMemoryCli(program: Command) { .option("--json", "Print JSON") .option("--deep", "Probe embedding provider availability") .option("--index", "Reindex if dirty (implies --deep)") - .option("--index-mode ", "Index mode (auto|batch|direct) when indexing", "auto") - .option("--progress ", "Progress output (auto|line|log|none)", "auto") .option("--verbose", "Verbose logging", false) .action(async (opts: MemoryCommandOptions) => { setVerbose(Boolean(opts.verbose)); - const rawCfg = loadConfig(); - const agentId = resolveAgent(rawCfg, opts.agent); - const indexMode = resolveIndexMode(opts.indexMode); - const progressMode = resolveProgressMode(opts.progress); - const progressOptions = resolveProgressOptions(progressMode, Boolean(opts.verbose)); - const cfg = applyIndexMode(rawCfg, agentId, indexMode); - await withManager({ - getManager: () => getMemorySearchManager({ cfg, agentId }), - onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."), - onCloseError: (err) => - defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`), - close: (manager) => manager.close(), - run: async (manager) => { - const deep = Boolean(opts.deep || opts.index); - let embeddingProbe: - | Awaited> - | undefined; - let indexError: string | undefined; - if (deep) { - await withProgress( - { label: "Checking memory…", total: 2, ...progressOptions }, - async (progress) => { + const cfg = loadConfig(); + const agentIds = resolveAgentIds(cfg, opts.agent); + const allResults: Array<{ + agentId: string; + status: ReturnType; + embeddingProbe?: Awaited>; + indexError?: string; + }> = []; + + for (const agentId of agentIds) { + await withManager({ + getManager: () => getMemorySearchManager({ cfg, agentId }), + onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."), + onCloseError: (err) => + defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`), + close: (manager) => manager.close(), + run: async (manager) => { + const deep = Boolean(opts.deep || opts.index); + let embeddingProbe: + | Awaited> + | undefined; + let indexError: string | undefined; + 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) { - await withProgressTotals( - { - label: "Indexing memory…", - total: 0, - ...progressOptions, - }, - async (update, progress) => { - try { - await manager.sync({ - reason: "cli", - 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; - } - }, - ); + }); + if (opts.index) { + await withProgressTotals( + { + label: "Indexing memory…", + total: 0, + fallback: opts.verbose ? "line" : undefined, + }, + async (update, progress) => { + try { + await manager.sync({ + reason: "cli", + 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 { + await manager.probeVectorAvailability(); } - } else { - await manager.probeVectorAvailability(); + const status = manager.status(); + allResults.push({ agentId, status, embeddingProbe, indexError }); + }, + }); + } + + if (opts.json) { + defaultRuntime.log(JSON.stringify(allResults, null, 2)); + return; + } + + const rich = isRich(); + const heading = (text: string) => colorize(rich, theme.heading, text); + const muted = (text: string) => colorize(rich, theme.muted, text); + const info = (text: string) => colorize(rich, theme.info, text); + const success = (text: string) => colorize(rich, theme.success, text); + const warn = (text: string) => colorize(rich, theme.warn, text); + const accent = (text: string) => colorize(rich, theme.accent, text); + const label = (text: string) => muted(`${text}:`); + + for (const result of allResults) { + const { agentId, status, embeddingProbe, indexError } = result; + if (opts.index) { + const line = indexError + ? `Memory index failed: ${indexError}` + : "Memory index complete."; + defaultRuntime.log(line); + } + const lines = [ + `${heading("Memory Search")} ${muted(`(${agentId})`)}`, + `${label("Provider")} ${info(status.provider)} ${muted( + `(requested: ${status.requestedProvider})`, + )}`, + `${label("Model")} ${info(status.model)}`, + status.sources?.length + ? `${label("Sources")} ${info(status.sources.join(", "))}` + : null, + `${label("Indexed")} ${success(`${status.files} files · ${status.chunks} chunks`)}`, + `${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`, + `${label("Store")} ${info(status.dbPath)}`, + `${label("Workspace")} ${info(status.workspaceDir)}`, + ].filter(Boolean) as string[]; + 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)}`); } - const status = manager.status(); - if (opts.json) { - defaultRuntime.log( - JSON.stringify( - { - ...status, - embeddings: embeddingProbe - ? { ok: embeddingProbe.ok, error: embeddingProbe.error } - : undefined, - indexError, - }, - null, - 2, - ), - ); - return; + } + if (status.sourceCounts?.length) { + lines.push(label("By source")); + for (const entry of status.sourceCounts) { + const counts = `${entry.files} files · ${entry.chunks} chunks`; + lines.push(` ${accent(entry.source)} ${muted("·")} ${muted(counts)}`); } - if (opts.index) { - const line = indexError - ? `Memory index failed: ${indexError}` - : "Memory index complete."; - defaultRuntime.log(line); + } + if (status.fallback) { + lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`); + } + if (status.vector) { + const vectorState = status.vector.enabled + ? 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))}`); } - const rich = isRich(); - const heading = (text: string) => colorize(rich, theme.heading, text); - const muted = (text: string) => colorize(rich, theme.muted, text); - const info = (text: string) => colorize(rich, theme.info, text); - const success = (text: string) => colorize(rich, theme.success, text); - const warn = (text: string) => colorize(rich, theme.warn, text); - const accent = (text: string) => colorize(rich, theme.accent, text); - const label = (text: string) => muted(`${text}:`); - const colorForTone = (tone: Tone) => - tone === "ok" ? theme.success : tone === "warn" ? theme.warn : theme.muted; - const lines = [ - `${heading("Memory Search")} ${muted(`(${agentId})`)}`, - `${label("Provider")} ${info(status.provider)} ${muted( - `(requested: ${status.requestedProvider})`, - )}`, - `${label("Model")} ${info(status.model)}`, - status.sources?.length - ? `${label("Sources")} ${info(status.sources.join(", "))}` - : null, - `${label("Indexed")} ${success(`${status.files} files · ${status.chunks} chunks`)}`, - `${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`, - `${label("Store")} ${info(status.dbPath)}`, - `${label("Workspace")} ${info(status.workspaceDir)}`, - ].filter(Boolean) as string[]; - 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.vector.extensionPath) { + lines.push(`${label("Vector path")} ${info(status.vector.extensionPath)}`); } - if (status.sourceCounts?.length) { - lines.push(label("By source")); - for (const entry of status.sourceCounts) { - const counts = `${entry.files} files · ${entry.chunks} chunks`; - lines.push(` ${accent(entry.source)} ${muted("·")} ${muted(counts)}`); - } + if (status.vector.loadError) { + lines.push(`${label("Vector error")} ${warn(status.vector.loadError)}`); } - if (status.fallback) { - lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`); + } + 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.vector) { - const vectorState = resolveMemoryVectorState(status.vector); - const vectorColor = colorForTone(vectorState.tone); - lines.push(`${label("Vector")} ${colorize(rich, vectorColor, vectorState.state)}`); - if (status.vector.dims) { - lines.push(`${label("Vector dims")} ${info(String(status.vector.dims))}`); - } - if (status.vector.extensionPath) { - lines.push(`${label("Vector path")} ${info(status.vector.extensionPath)}`); - } - if (status.vector.loadError) { - lines.push(`${label("Vector error")} ${warn(status.vector.loadError)}`); - } + } + 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.fts) { - const ftsState = resolveMemoryFtsState(status.fts); - const ftsColor = colorForTone(ftsState.tone); - lines.push(`${label("FTS")} ${colorize(rich, ftsColor, ftsState.state)}`); - if (status.fts.error) { - lines.push(`${label("FTS error")} ${warn(status.fts.error)}`); - } - } - if (status.cache) { - const cacheState = resolveMemoryCacheState(status.cache); - const cacheColor = colorForTone(cacheState.tone); - const suffix = - status.cache.enabled && typeof status.cache.entries === "number" - ? ` (${status.cache.entries} entries)` - : ""; - lines.push( - `${label("Embedding cache")} ${colorize(rich, cacheColor, cacheState.state)}${suffix}`, - ); - if (status.cache.enabled && typeof status.cache.maxEntries === "number") { - lines.push(`${label("Cache cap")} ${info(String(status.cache.maxEntries))}`); - } - } - if (status.fallback?.reason) { - lines.push(muted(status.fallback.reason)); - } - if (indexError) { - lines.push(`${label("Index error")} ${warn(indexError)}`); - } - defaultRuntime.log(lines.join("\n")); - }, - }); + } + if (status.fallback?.reason) { + lines.push(muted(status.fallback.reason)); + } + if (indexError) { + lines.push(`${label("Index error")} ${warn(indexError)}`); + } + defaultRuntime.log(lines.join("\n")); + if (agentIds.length > 1) defaultRuntime.log(""); + } }); memory @@ -300,56 +263,57 @@ export function registerMemoryCli(program: Command) { .description("Reindex memory files") .option("--agent ", "Agent id (default: default agent)") .option("--force", "Force full reindex", false) - .option("--index-mode ", "Index mode (auto|batch|direct) when indexing", "auto") - .option("--progress ", "Progress output (auto|line|log|none)", "auto") + .option("--verbose", "Verbose logging", false) .action(async (opts: MemoryCommandOptions & { force?: boolean }) => { - const rawCfg = loadConfig(); - const agentId = resolveAgent(rawCfg, opts.agent); - const indexMode = resolveIndexMode(opts.indexMode); - const progressMode = resolveProgressMode(opts.progress); - const progressOptions = resolveProgressOptions(progressMode, Boolean(opts.verbose)); - const cfg = applyIndexMode(rawCfg, agentId, indexMode); - await withManager({ - getManager: () => getMemorySearchManager({ cfg, agentId }), - onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."), - onCloseError: (err) => - defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`), - close: (manager) => manager.close(), - run: async (manager) => { - try { - if (progressMode === "none") { + setVerbose(Boolean(opts.verbose)); + const cfg = loadConfig(); + const agentIds = resolveAgentIds(cfg, opts.agent); + for (const agentId of agentIds) { + await withManager({ + getManager: () => getMemorySearchManager({ cfg, agentId }), + onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."), + onCloseError: (err) => + defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`), + close: (manager) => manager.close(), + run: async (manager) => { + try { + if (opts.verbose) { + const status = manager.status(); + const rich = isRich(); + const heading = (text: string) => colorize(rich, theme.heading, text); + const muted = (text: string) => colorize(rich, theme.muted, text); + const info = (text: string) => colorize(rich, theme.info, text); + const warn = (text: string) => colorize(rich, theme.warn, text); + const label = (text: string) => muted(`${text}:`); + const sourceLabels = status.sources.map((source) => + formatSourceLabel(source, status.workspaceDir, agentId), + ); + const lines = [ + `${heading("Memory Index")} ${muted(`(${agentId})`)}`, + `${label("Provider")} ${info(status.provider)} ${muted( + `(requested: ${status.requestedProvider})`, + )}`, + `${label("Model")} ${info(status.model)}`, + sourceLabels.length + ? `${label("Sources")} ${info(sourceLabels.join(", "))}` + : null, + ].filter(Boolean) as string[]; + if (status.fallback) { + lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`); + } + defaultRuntime.log(lines.join("\n")); + defaultRuntime.log(""); + } await manager.sync({ reason: "cli", force: opts.force }); - } else { - await withProgressTotals( - { - label: "Indexing memory…", - total: 0, - ...progressOptions, - }, - async (update, progress) => { - await manager.sync({ - reason: "cli", - force: opts.force, - progress: (syncUpdate) => { - update({ - completed: syncUpdate.completed, - total: syncUpdate.total, - label: syncUpdate.label, - }); - if (syncUpdate.label) progress.setLabel(syncUpdate.label); - }, - }); - }, - ); + defaultRuntime.log(`Memory index updated (${agentId}).`); + } catch (err) { + const message = formatErrorMessage(err); + defaultRuntime.error(`Memory index failed (${agentId}): ${message}`); + process.exitCode = 1; } - defaultRuntime.log("Memory index updated."); - } catch (err) { - const message = formatErrorMessage(err); - defaultRuntime.error(`Memory index failed: ${message}`); - process.exitCode = 1; - } - }, - }); + }, + }); + } }); memory