fix: close memory cli managers
This commit is contained in:
@@ -26,6 +26,7 @@ describe("memory cli", () => {
|
|||||||
it("prints vector status when available", async () => {
|
it("prints vector status when available", async () => {
|
||||||
const { registerMemoryCli } = await import("./memory-cli.js");
|
const { registerMemoryCli } = await import("./memory-cli.js");
|
||||||
const { defaultRuntime } = await import("../runtime.js");
|
const { defaultRuntime } = await import("../runtime.js");
|
||||||
|
const close = vi.fn(async () => {});
|
||||||
getMemorySearchManager.mockResolvedValueOnce({
|
getMemorySearchManager.mockResolvedValueOnce({
|
||||||
manager: {
|
manager: {
|
||||||
status: () => ({
|
status: () => ({
|
||||||
@@ -44,6 +45,7 @@ describe("memory cli", () => {
|
|||||||
dims: 1024,
|
dims: 1024,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
close,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,11 +58,13 @@ describe("memory cli", () => {
|
|||||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready"));
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready"));
|
||||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector dims: 1024"));
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector dims: 1024"));
|
||||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector path: /opt/sqlite-vec.dylib"));
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector path: /opt/sqlite-vec.dylib"));
|
||||||
|
expect(close).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prints vector error when unavailable", async () => {
|
it("prints vector error when unavailable", async () => {
|
||||||
const { registerMemoryCli } = await import("./memory-cli.js");
|
const { registerMemoryCli } = await import("./memory-cli.js");
|
||||||
const { defaultRuntime } = await import("../runtime.js");
|
const { defaultRuntime } = await import("../runtime.js");
|
||||||
|
const close = vi.fn(async () => {});
|
||||||
getMemorySearchManager.mockResolvedValueOnce({
|
getMemorySearchManager.mockResolvedValueOnce({
|
||||||
manager: {
|
manager: {
|
||||||
status: () => ({
|
status: () => ({
|
||||||
@@ -78,6 +82,7 @@ describe("memory cli", () => {
|
|||||||
loadError: "load failed",
|
loadError: "load failed",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
close,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,5 +94,6 @@ describe("memory cli", () => {
|
|||||||
|
|
||||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: unavailable"));
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: unavailable"));
|
||||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector error: load failed"));
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector error: load failed"));
|
||||||
|
expect(close).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import chalk from "chalk";
|
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
|
|
||||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
@@ -6,7 +5,7 @@ import { loadConfig } from "../config/config.js";
|
|||||||
import { getMemorySearchManager } from "../memory/index.js";
|
import { getMemorySearchManager } from "../memory/index.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { formatDocsLink } from "../terminal/links.js";
|
import { formatDocsLink } from "../terminal/links.js";
|
||||||
import { theme } from "../terminal/theme.js";
|
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||||
|
|
||||||
type MemoryCommandOptions = {
|
type MemoryCommandOptions = {
|
||||||
agent?: string;
|
agent?: string;
|
||||||
@@ -42,42 +41,72 @@ export function registerMemoryCli(program: Command) {
|
|||||||
defaultRuntime.log(error ?? "Memory search disabled.");
|
defaultRuntime.log(error ?? "Memory search disabled.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const status = manager.status();
|
try {
|
||||||
if (opts.json) {
|
const status = manager.status();
|
||||||
defaultRuntime.log(JSON.stringify(status, null, 2));
|
if (opts.json) {
|
||||||
return;
|
defaultRuntime.log(JSON.stringify(status, null, 2));
|
||||||
}
|
return;
|
||||||
const lines = [
|
|
||||||
`${chalk.bold.cyan("Memory Search")} (${agentId})`,
|
|
||||||
`Provider: ${status.provider} (requested: ${status.requestedProvider})`,
|
|
||||||
status.fallback ? chalk.yellow(`Fallback: ${status.fallback.from}`) : null,
|
|
||||||
status.sources?.length ? `Sources: ${status.sources.join(", ")}` : null,
|
|
||||||
`Files: ${status.files}`,
|
|
||||||
`Chunks: ${status.chunks}`,
|
|
||||||
`Dirty: ${status.dirty ? "yes" : "no"}`,
|
|
||||||
`Index: ${status.dbPath}`,
|
|
||||||
].filter(Boolean) as string[];
|
|
||||||
if (status.vector) {
|
|
||||||
const vectorState = status.vector.enabled
|
|
||||||
? status.vector.available
|
|
||||||
? "ready"
|
|
||||||
: "unavailable"
|
|
||||||
: "disabled";
|
|
||||||
lines.push(`Vector: ${vectorState}`);
|
|
||||||
if (status.vector.dims) {
|
|
||||||
lines.push(`Vector dims: ${status.vector.dims}`);
|
|
||||||
}
|
}
|
||||||
if (status.vector.extensionPath) {
|
const rich = isRich();
|
||||||
lines.push(`Vector path: ${status.vector.extensionPath}`);
|
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 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 (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) {
|
if (status.fallback) {
|
||||||
lines.push(chalk.yellow(`Vector error: ${status.vector.loadError}`));
|
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))}`);
|
||||||
|
}
|
||||||
|
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.fallback?.reason) {
|
||||||
|
lines.push(muted(status.fallback.reason));
|
||||||
|
}
|
||||||
|
defaultRuntime.log(lines.join("\n"));
|
||||||
|
} finally {
|
||||||
|
await manager.close();
|
||||||
}
|
}
|
||||||
if (status.fallback?.reason) {
|
|
||||||
lines.push(chalk.gray(status.fallback.reason));
|
|
||||||
}
|
|
||||||
defaultRuntime.log(lines.join("\n"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
memory
|
memory
|
||||||
@@ -100,6 +129,8 @@ export function registerMemoryCli(program: Command) {
|
|||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
defaultRuntime.error(`Memory index failed: ${message}`);
|
defaultRuntime.error(`Memory index failed: ${message}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
} finally {
|
||||||
|
await manager.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,6 +171,8 @@ export function registerMemoryCli(program: Command) {
|
|||||||
defaultRuntime.error(`Memory search failed: ${message}`);
|
defaultRuntime.error(`Memory search failed: ${message}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
|
} finally {
|
||||||
|
await manager.close();
|
||||||
}
|
}
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
defaultRuntime.log(JSON.stringify({ results }, null, 2));
|
defaultRuntime.log(JSON.stringify({ results }, null, 2));
|
||||||
@@ -149,12 +182,17 @@ export function registerMemoryCli(program: Command) {
|
|||||||
defaultRuntime.log("No matches.");
|
defaultRuntime.log("No matches.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const rich = isRich();
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
lines.push(
|
lines.push(
|
||||||
`${chalk.green(result.score.toFixed(3))} ${result.path}:${result.startLine}-${result.endLine}`,
|
`${colorize(rich, theme.success, result.score.toFixed(3))} ${colorize(
|
||||||
|
rich,
|
||||||
|
theme.accent,
|
||||||
|
`${result.path}:${result.startLine}-${result.endLine}`,
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
lines.push(chalk.gray(result.snippet));
|
lines.push(colorize(rich, theme.muted, result.snippet));
|
||||||
lines.push("");
|
lines.push("");
|
||||||
}
|
}
|
||||||
defaultRuntime.log(lines.join("\n").trim());
|
defaultRuntime.log(lines.join("\n").trim());
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ export class MemoryIndexManager {
|
|||||||
model: string;
|
model: string;
|
||||||
requestedProvider: string;
|
requestedProvider: string;
|
||||||
sources: MemorySource[];
|
sources: MemorySource[];
|
||||||
|
sourceCounts: Array<{ source: MemorySource; files: number; chunks: number }>;
|
||||||
fallback?: { from: string; reason?: string };
|
fallback?: { from: string; reason?: string };
|
||||||
vector?: {
|
vector?: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -317,6 +318,35 @@ export class MemoryIndexManager {
|
|||||||
.get(...sourceFilter.params) as {
|
.get(...sourceFilter.params) as {
|
||||||
c: number;
|
c: number;
|
||||||
};
|
};
|
||||||
|
const sourceCounts = (() => {
|
||||||
|
const sources = Array.from(this.sources);
|
||||||
|
if (sources.length === 0) return [];
|
||||||
|
const bySource = new Map<MemorySource, { files: number; chunks: number }>();
|
||||||
|
for (const source of sources) {
|
||||||
|
bySource.set(source, { files: 0, chunks: 0 });
|
||||||
|
}
|
||||||
|
const fileRows = this.db
|
||||||
|
.prepare(
|
||||||
|
`SELECT source, COUNT(*) as c FROM files WHERE 1=1${sourceFilter.sql} GROUP BY source`,
|
||||||
|
)
|
||||||
|
.all(...sourceFilter.params) as Array<{ source: MemorySource; c: number }>;
|
||||||
|
for (const row of fileRows) {
|
||||||
|
const entry = bySource.get(row.source) ?? { files: 0, chunks: 0 };
|
||||||
|
entry.files = row.c ?? 0;
|
||||||
|
bySource.set(row.source, entry);
|
||||||
|
}
|
||||||
|
const chunkRows = this.db
|
||||||
|
.prepare(
|
||||||
|
`SELECT source, COUNT(*) as c FROM chunks WHERE 1=1${sourceFilter.sql} GROUP BY source`,
|
||||||
|
)
|
||||||
|
.all(...sourceFilter.params) as Array<{ source: MemorySource; c: number }>;
|
||||||
|
for (const row of chunkRows) {
|
||||||
|
const entry = bySource.get(row.source) ?? { files: 0, chunks: 0 };
|
||||||
|
entry.chunks = row.c ?? 0;
|
||||||
|
bySource.set(row.source, entry);
|
||||||
|
}
|
||||||
|
return sources.map((source) => ({ source, ...bySource.get(source)! }));
|
||||||
|
})();
|
||||||
return {
|
return {
|
||||||
files: files?.c ?? 0,
|
files: files?.c ?? 0,
|
||||||
chunks: chunks?.c ?? 0,
|
chunks: chunks?.c ?? 0,
|
||||||
@@ -327,6 +357,7 @@ export class MemoryIndexManager {
|
|||||||
model: this.provider.model,
|
model: this.provider.model,
|
||||||
requestedProvider: this.requestedProvider,
|
requestedProvider: this.requestedProvider,
|
||||||
sources: Array.from(this.sources),
|
sources: Array.from(this.sources),
|
||||||
|
sourceCounts,
|
||||||
fallback: this.fallbackReason ? { from: "local", reason: this.fallbackReason } : undefined,
|
fallback: this.fallbackReason ? { from: "local", reason: this.fallbackReason } : undefined,
|
||||||
vector: {
|
vector: {
|
||||||
enabled: this.vector.enabled,
|
enabled: this.vector.enabled,
|
||||||
|
|||||||
Reference in New Issue
Block a user