diff --git a/CHANGELOG.md b/CHANGELOG.md index dc780fd2a..22d6474f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Docs: https://docs.clawd.bot - macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x. - macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg. - CLI: surface update availability in `clawdbot status`. +- CLI: add `clawdbot memory status --deep/--index` probes. ### Fixes - Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos. diff --git a/docs/cli/memory.md b/docs/cli/memory.md index 89fb5bf39..5be472d4c 100644 --- a/docs/cli/memory.md +++ b/docs/cli/memory.md @@ -16,7 +16,8 @@ Related: ```bash clawdbot memory status +clawdbot memory status --deep +clawdbot memory status --deep --index clawdbot memory index clawdbot memory search "release checklist" ``` - diff --git a/src/cli/memory-cli.test.ts b/src/cli/memory-cli.test.ts index 45638257d..5397878f2 100644 --- a/src/cli/memory-cli.test.ts +++ b/src/cli/memory-cli.test.ts @@ -100,6 +100,78 @@ describe("memory cli", () => { expect(close).toHaveBeenCalled(); }); + it("prints embeddings status when deep", async () => { + const { registerMemoryCli } = await import("./memory-cli.js"); + const { defaultRuntime } = await import("../runtime.js"); + const close = vi.fn(async () => {}); + const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true })); + getMemorySearchManager.mockResolvedValueOnce({ + manager: { + probeVectorAvailability: vi.fn(async () => true), + probeEmbeddingAvailability, + status: () => ({ + files: 1, + chunks: 1, + dirty: false, + workspaceDir: "/tmp/clawd", + dbPath: "/tmp/memory.sqlite", + provider: "openai", + model: "text-embedding-3-small", + requestedProvider: "openai", + vector: { enabled: true, available: true }, + }), + close, + }, + }); + + const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); + const program = new Command(); + program.name("test"); + registerMemoryCli(program); + await program.parseAsync(["memory", "status", "--deep"], { from: "user" }); + + expect(probeEmbeddingAvailability).toHaveBeenCalled(); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Embeddings: ready")); + expect(close).toHaveBeenCalled(); + }); + + it("reindexes on status --index", async () => { + const { registerMemoryCli } = await import("./memory-cli.js"); + const { defaultRuntime } = await import("../runtime.js"); + const close = vi.fn(async () => {}); + const sync = vi.fn(async () => {}); + const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true })); + getMemorySearchManager.mockResolvedValueOnce({ + manager: { + probeVectorAvailability: vi.fn(async () => true), + probeEmbeddingAvailability, + sync, + status: () => ({ + files: 1, + chunks: 1, + dirty: false, + workspaceDir: "/tmp/clawd", + dbPath: "/tmp/memory.sqlite", + provider: "openai", + model: "text-embedding-3-small", + requestedProvider: "openai", + vector: { enabled: true, available: true }, + }), + close, + }, + }); + + vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); + const program = new Command(); + program.name("test"); + registerMemoryCli(program); + await program.parseAsync(["memory", "status", "--index"], { from: "user" }); + + expect(sync).toHaveBeenCalledWith({ reason: "cli" }); + expect(probeEmbeddingAvailability).toHaveBeenCalled(); + expect(close).toHaveBeenCalled(); + }); + it("closes manager after index", async () => { const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); diff --git a/src/cli/memory-cli.ts b/src/cli/memory-cli.ts index 0daef475f..9861a60ae 100644 --- a/src/cli/memory-cli.ts +++ b/src/cli/memory-cli.ts @@ -10,6 +10,8 @@ import { colorize, isRich, theme } from "../terminal/theme.js"; type MemoryCommandOptions = { agent?: string; json?: boolean; + deep?: boolean; + index?: boolean; }; function resolveAgent(cfg: ReturnType, agent?: string) { @@ -33,6 +35,8 @@ export function registerMemoryCli(program: Command) { .description("Show memory search index status") .option("--agent ", "Agent id (default: default agent)") .option("--json", "Print JSON") + .option("--deep", "Probe embedding provider availability") + .option("--index", "Reindex if dirty (implies --deep)") .action(async (opts: MemoryCommandOptions) => { const cfg = loadConfig(); const agentId = resolveAgent(cfg, opts.agent); @@ -43,9 +47,33 @@ export function registerMemoryCli(program: Command) { } try { await manager.probeVectorAvailability(); + const deep = Boolean(opts.deep || opts.index); + const embeddingProbe = deep ? await manager.probeEmbeddingAvailability() : undefined; + let indexError: string | undefined; + if (opts.index) { + try { + await manager.sync({ reason: "cli" }); + } catch (err) { + indexError = err instanceof Error ? err.message : String(err); + defaultRuntime.error(`Memory index failed: ${indexError}`); + process.exitCode = 1; + } + } const status = manager.status(); if (opts.json) { - defaultRuntime.log(JSON.stringify(status, null, 2)); + defaultRuntime.log( + JSON.stringify( + { + ...status, + embeddings: embeddingProbe + ? { ok: embeddingProbe.ok, error: embeddingProbe.error } + : undefined, + indexError, + }, + null, + 2, + ), + ); return; } const rich = isRich(); @@ -68,6 +96,14 @@ export function registerMemoryCli(program: Command) { `${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.sourceCounts?.length) { lines.push(label("By source")); for (const entry of status.sourceCounts) { @@ -104,6 +140,9 @@ export function registerMemoryCli(program: Command) { if (status.fallback?.reason) { lines.push(muted(status.fallback.reason)); } + if (indexError) { + lines.push(`${label("Index error")} ${warn(indexError)}`); + } defaultRuntime.log(lines.join("\n")); } finally { await manager.close(); diff --git a/src/memory/manager.ts b/src/memory/manager.ts index e01e27376..ed979f0d5 100644 --- a/src/memory/manager.ts +++ b/src/memory/manager.ts @@ -377,6 +377,16 @@ export class MemoryIndexManager { return this.ensureVectorReady(); } + async probeEmbeddingAvailability(): Promise<{ ok: boolean; error?: string }> { + try { + await this.provider.embedQuery("ping"); + return { ok: true }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { ok: false, error: message }; + } + } + async close(): Promise { if (this.closed) return; this.closed = true;