fix(pi-agent-plugin): make command results visible and relevance-filtered (#5504)
This commit is contained in:
@@ -55,6 +55,7 @@ const defaultConfig: Mem0Config = {
|
||||
autoCapture: false,
|
||||
defaultScope: "project",
|
||||
contextInjection: false,
|
||||
searchThreshold: 0.3,
|
||||
dream: { enabled: false, auto: false, minHours: 24, minSessions: 5, minMemories: 20 },
|
||||
};
|
||||
|
||||
@@ -67,6 +68,7 @@ describe("registerCommands", () => {
|
||||
beforeEach(() => {
|
||||
pi = makePi();
|
||||
mem0 = makeMem0();
|
||||
defaultConfig.defaultScope = "project";
|
||||
registerCommands(pi as any, mem0, defaultConfig, () => scopeCtx);
|
||||
});
|
||||
|
||||
@@ -90,11 +92,17 @@ describe("registerCommands", () => {
|
||||
expect(mem0.search).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("notifies when no memories match", async () => {
|
||||
it("sends a visible message naming the query when no memories match", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [] });
|
||||
await pi._invoke("mem0-forget", "old preference", ctx);
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("No matching memories found.", "info");
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-forget",
|
||||
content: expect.stringContaining('No matches for "old preference"'),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("asks for confirmation before deleting a single match", async () => {
|
||||
@@ -111,6 +119,22 @@ describe("registerCommands", () => {
|
||||
expect(mem0.delete).toHaveBeenCalledWith("abc-123");
|
||||
});
|
||||
|
||||
it("sends a visible confirmation showing what was forgotten", async () => {
|
||||
const ctx = makeCtx(true);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] });
|
||||
mem0.delete.mockResolvedValue({ message: "Deleted" });
|
||||
|
||||
await pi._invoke("mem0-forget", "test", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-forget",
|
||||
content: expect.stringContaining("Forgotten"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not delete when user cancels confirmation", async () => {
|
||||
const ctx = makeCtx(false);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] });
|
||||
@@ -119,7 +143,9 @@ describe("registerCommands", () => {
|
||||
|
||||
expect(ctx.ui.confirm).toHaveBeenCalled();
|
||||
expect(mem0.delete).not.toHaveBeenCalled();
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info");
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses select UI for multiple matches and deletes chosen memory", async () => {
|
||||
@@ -136,13 +162,20 @@ describe("registerCommands", () => {
|
||||
await pi._invoke("mem0-forget", "test", ctx);
|
||||
|
||||
expect(ctx.ui.select).toHaveBeenCalledWith(
|
||||
"Which memory should I delete?",
|
||||
expect.stringContaining("which should I delete"),
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("mem one"),
|
||||
expect.stringContaining("mem two"),
|
||||
]),
|
||||
);
|
||||
expect(mem0.delete).toHaveBeenCalledWith("id-2");
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-forget",
|
||||
content: expect.stringContaining("Forgotten"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not delete when user cancels select", async () => {
|
||||
@@ -158,7 +191,9 @@ describe("registerCommands", () => {
|
||||
await pi._invoke("mem0-forget", "test", ctx);
|
||||
|
||||
expect(mem0.delete).not.toHaveBeenCalled();
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info");
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -179,6 +214,22 @@ describe("registerCommands", () => {
|
||||
expect(mem0.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends a visible confirmation after pinning", async () => {
|
||||
const ctx = makeCtx(true);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "important fact" }] });
|
||||
mem0.update.mockResolvedValue([]);
|
||||
|
||||
await pi._invoke("mem0-pin", "important", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-pin",
|
||||
content: expect.stringContaining("Pinned"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not pin when user cancels", async () => {
|
||||
const ctx = makeCtx(false);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "fact" }] });
|
||||
@@ -188,7 +239,7 @@ describe("registerCommands", () => {
|
||||
expect(mem0.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips already-pinned memories", async () => {
|
||||
it("skips already-pinned memories with a visible message", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "[PINNED] fact" }] });
|
||||
|
||||
@@ -196,7 +247,9 @@ describe("registerCommands", () => {
|
||||
|
||||
expect(ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(mem0.add).not.toHaveBeenCalled();
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("Already pinned.", "info");
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("Already pinned"), display: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses select UI for multiple matches and pins chosen memory", async () => {
|
||||
@@ -213,7 +266,7 @@ describe("registerCommands", () => {
|
||||
await pi._invoke("mem0-pin", "fact", ctx);
|
||||
|
||||
expect(ctx.ui.select).toHaveBeenCalledWith(
|
||||
"Which memory should I pin?",
|
||||
expect.stringContaining("which should I pin"),
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("fact one"),
|
||||
expect.stringContaining("fact two"),
|
||||
@@ -235,18 +288,23 @@ describe("registerCommands", () => {
|
||||
await pi._invoke("mem0-pin", "fact", ctx);
|
||||
|
||||
expect(mem0.update).not.toHaveBeenCalled();
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info");
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-search", () => {
|
||||
it("always performs semantic search", async () => {
|
||||
it("performs server-side semantic search with a relevance threshold", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "id-1", memory: "result" }] });
|
||||
|
||||
await pi._invoke("mem0-search", "my preferences", ctx);
|
||||
|
||||
expect(mem0.search).toHaveBeenCalledWith("my preferences", expect.any(Object));
|
||||
expect(mem0.search).toHaveBeenCalledWith(
|
||||
"my preferences",
|
||||
expect.objectContaining({ threshold: 0.3, topK: 10, rerank: true }),
|
||||
);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ customType: "mem0-search" }),
|
||||
);
|
||||
@@ -263,16 +321,49 @@ describe("registerCommands", () => {
|
||||
expect(mem0.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows empty results message", async () => {
|
||||
it("shows a no-matches message naming the query", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [] });
|
||||
|
||||
await pi._invoke("mem0-search", "nonexistent", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: "No memories found." }),
|
||||
expect.objectContaining({ content: expect.stringContaining("No matches") }),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a result count header when there are matches", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "one" },
|
||||
{ id: "id-2", memory: "two" },
|
||||
],
|
||||
});
|
||||
|
||||
await pi._invoke("mem0-search", "stuff", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("2 matches") }),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows all results the API returns (relevance gating is server-side)", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "first match", score: 0.62 },
|
||||
{ id: "id-2", memory: "second match", score: 0.31 },
|
||||
],
|
||||
});
|
||||
|
||||
await pi._invoke("mem0-search", "stuff", ctx);
|
||||
|
||||
const call = pi.sendMessage.mock.calls.find(([m]: any[]) => m.customType === "mem0-search");
|
||||
expect(call?.[0].content).toContain("first match");
|
||||
expect(call?.[0].content).toContain("second match");
|
||||
expect(call?.[0].content).toContain("2 matches");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-remember", () => {
|
||||
@@ -288,10 +379,132 @@ describe("registerCommands", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the stored text in a visible confirmation (infer:false status response)", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.add.mockResolvedValue({ message: "Memories stored successfully" });
|
||||
|
||||
await pi._invoke("mem0-remember", "I prefer dark mode", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-remember",
|
||||
content: expect.stringContaining("I prefer dark mode"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("lists memory objects returned by the API when present", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.add.mockResolvedValue([{ id: "m1", memory: "Uses dark mode", event: "ADD" }]);
|
||||
|
||||
await pi._invoke("mem0-remember", "I prefer dark mode", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-remember",
|
||||
content: expect.stringContaining("Uses dark mode"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows warning when no text provided", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-remember", " ", ctx);
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /mem0-remember <text>", "warning");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-scope", () => {
|
||||
it("sends a visible message showing the current scope when no arg is given", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-scope", "", ctx);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-scope",
|
||||
content: expect.stringContaining("Current scope:"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a visible confirmation after changing scope", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-scope", "global", ctx);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-scope",
|
||||
content: expect.stringContaining("Scope changed to global"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns on an invalid scope", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-scope", "bogus", ctx);
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid scope "bogus"'),
|
||||
"warning",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-tour", () => {
|
||||
it("shows an empty-state message when there are no memories", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.getAll.mockResolvedValue({ results: [] });
|
||||
|
||||
await pi._invoke("mem0-tour", "", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-tour",
|
||||
content: expect.stringContaining("No memories"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("groups memories by category with a count header", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.getAll.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "likes tea", categories: ["preferences"] },
|
||||
{ id: "id-2", memory: "uses vim", categories: ["technical"] },
|
||||
],
|
||||
});
|
||||
|
||||
await pi._invoke("mem0-tour", "", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-tour",
|
||||
content: expect.stringContaining("Memory tour"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-dream", () => {
|
||||
it("feeds the protocol to the agent and shows a clean status line", async () => {
|
||||
const ctx = makeCtx();
|
||||
|
||||
await pi._invoke("mem0-dream", "", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ customType: "mem0-dream", display: false }),
|
||||
expect.objectContaining({ triggerTurn: true }),
|
||||
);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-dream",
|
||||
content: expect.stringContaining("Dreaming"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ import { acquireDreamLock } from "./dream/index.ts";
|
||||
import { CONFIG_DIR } from "./config/index.ts";
|
||||
import { captureCommandEvent } from "./telemetry.ts";
|
||||
|
||||
const SEARCH_TOP_K = 10;
|
||||
|
||||
export function registerCommands(
|
||||
pi: ExtensionAPI,
|
||||
mem0: MemoryClient,
|
||||
@@ -16,7 +18,24 @@ export function registerCommands(
|
||||
getScopeCtx: () => ScopeContext,
|
||||
telemetryCtx?: { apiKey?: string },
|
||||
): void {
|
||||
// ── /mem0-remember ──────────────────────────────────────────────────
|
||||
const sendFeedback = (customType: string, content: string): void => {
|
||||
pi.sendMessage({ customType, content, display: true });
|
||||
};
|
||||
|
||||
const pluralize = (n: number, one: string, many: string): string =>
|
||||
`${n} ${n === 1 ? one : many}`;
|
||||
|
||||
const searchMemories = async (query: string, scope: Scope) => {
|
||||
const filters = resolveSearchFilters(scope, getScopeCtx());
|
||||
const result = await mem0.search(query, {
|
||||
filters,
|
||||
threshold: config.searchThreshold,
|
||||
topK: SEARCH_TOP_K,
|
||||
rerank: true,
|
||||
});
|
||||
return result.results ?? [];
|
||||
};
|
||||
|
||||
pi.registerCommand("mem0-remember", {
|
||||
description: "Store a memory verbatim (no inference)",
|
||||
handler: async (args, ctx) => {
|
||||
@@ -26,19 +45,24 @@ export function registerCommands(
|
||||
return;
|
||||
}
|
||||
|
||||
const scopeCtx = getScopeCtx();
|
||||
const addParams = resolveAddParams(config.defaultScope, scopeCtx);
|
||||
const addParams = resolveAddParams(config.defaultScope, getScopeCtx());
|
||||
const result = await mem0.add(
|
||||
[{ role: "user", content: text }],
|
||||
{ ...addParams, customCategories: DEFAULT_CUSTOM_CATEGORIES, infer: false },
|
||||
);
|
||||
const msg = (result as { message?: string }).message ?? "Memory stored.";
|
||||
captureCommandEvent("mem0-remember", {}, telemetryCtx);
|
||||
ctx.ui.notify(msg, "info");
|
||||
|
||||
const storedItems = (Array.isArray(result) ? result : [])
|
||||
.map((m) => (m as { memory?: string }).memory)
|
||||
.filter((m): m is string => Boolean(m));
|
||||
const items = storedItems.length > 0 ? storedItems : [text];
|
||||
sendFeedback(
|
||||
"mem0-remember",
|
||||
[`**Stored to ${config.defaultScope} memory**`, ...items.map((m) => `- ${m}`)].join("\n"),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// ── /mem0-forget ────────────────────────────────────────────────────
|
||||
pi.registerCommand("mem0-forget", {
|
||||
description: "Delete memories matching a natural language query",
|
||||
handler: async (args, ctx) => {
|
||||
@@ -48,49 +72,51 @@ export function registerCommands(
|
||||
return;
|
||||
}
|
||||
|
||||
const scopeCtx = getScopeCtx();
|
||||
const filters = resolveSearchFilters(config.defaultScope, scopeCtx);
|
||||
const result = await mem0.search(query, { filters });
|
||||
const memories = result.results ?? [];
|
||||
const memories = await searchMemories(query, config.defaultScope);
|
||||
|
||||
if (memories.length === 0) {
|
||||
captureCommandEvent("mem0-forget", { result_count: 0 }, telemetryCtx);
|
||||
ctx.ui.notify("No matching memories found.", "info");
|
||||
sendFeedback("mem0-forget", `**No matches for "${query}"** — nothing to forget.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const forgotten = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
|
||||
captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx);
|
||||
sendFeedback(
|
||||
"mem0-forget",
|
||||
[`**Forgotten from ${config.defaultScope} memory**`, `- ${formatMemoryCompact(mem)}`].join("\n"),
|
||||
);
|
||||
};
|
||||
|
||||
if (memories.length === 1) {
|
||||
const target = memories[0];
|
||||
const confirmed = await ctx.ui.confirm(
|
||||
"Delete this memory?",
|
||||
formatMemoryCompact(target),
|
||||
);
|
||||
const confirmed = await ctx.ui.confirm("Delete this memory?", formatMemoryCompact(target));
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Cancelled.", "info");
|
||||
sendFeedback("mem0-forget", "**Cancelled** — no memories deleted.");
|
||||
return;
|
||||
}
|
||||
await mem0.delete(target.id);
|
||||
captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx);
|
||||
ctx.ui.notify(`Deleted: ${formatMemoryCompact(target)}`, "info");
|
||||
forgotten(target);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = memories.map((m) => formatMemoryCompact(m));
|
||||
const selected = await ctx.ui.select("Which memory should I delete?", labels);
|
||||
const selected = await ctx.ui.select(
|
||||
`Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I delete?`,
|
||||
labels,
|
||||
);
|
||||
if (!selected) {
|
||||
ctx.ui.notify("Cancelled.", "info");
|
||||
sendFeedback("mem0-forget", "**Cancelled** — no memories deleted.");
|
||||
return;
|
||||
}
|
||||
const idx = labels.indexOf(selected);
|
||||
if (idx < 0) return;
|
||||
const target = memories[idx];
|
||||
await mem0.delete(target.id);
|
||||
captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx);
|
||||
ctx.ui.notify(`Deleted: ${formatMemoryCompact(target)}`, "info");
|
||||
forgotten(target);
|
||||
},
|
||||
});
|
||||
|
||||
// ── /mem0-search ────────────────────────────────────────────────────
|
||||
pi.registerCommand("mem0-search", {
|
||||
description: "Semantic search across memories",
|
||||
handler: async (args, ctx) => {
|
||||
@@ -100,21 +126,25 @@ export function registerCommands(
|
||||
return;
|
||||
}
|
||||
|
||||
const scopeCtx = getScopeCtx();
|
||||
const filters = resolveSearchFilters(config.defaultScope, scopeCtx);
|
||||
const result = await mem0.search(query, { filters });
|
||||
const memories = result.results ?? [];
|
||||
|
||||
const memories = await searchMemories(query, config.defaultScope);
|
||||
captureCommandEvent("mem0-search", { result_count: memories.length }, telemetryCtx);
|
||||
pi.sendMessage({
|
||||
customType: "mem0-search",
|
||||
content: formatMemoryList(memories),
|
||||
display: true,
|
||||
});
|
||||
|
||||
if (memories.length === 0) {
|
||||
sendFeedback("mem0-search", `**No matches for "${query}"** · ${config.defaultScope} scope`);
|
||||
return;
|
||||
}
|
||||
|
||||
sendFeedback(
|
||||
"mem0-search",
|
||||
[
|
||||
`**${pluralize(memories.length, "match", "matches")} for "${query}"** · ${config.defaultScope} scope`,
|
||||
"",
|
||||
formatMemoryList(memories),
|
||||
].join("\n"),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// ── /mem0-tour ──────────────────────────────────────────────────────
|
||||
pi.registerCommand("mem0-tour", {
|
||||
description: "Browse all memories grouped by category",
|
||||
handler: async (args, ctx) => {
|
||||
@@ -125,19 +155,21 @@ export function registerCommands(
|
||||
return;
|
||||
}
|
||||
const scope: Scope = (raw as Scope) || config.defaultScope;
|
||||
const scopeCtx = getScopeCtx();
|
||||
const filters = resolveSearchFilters(scope, scopeCtx);
|
||||
const filters = resolveSearchFilters(scope, getScopeCtx());
|
||||
const result = await mem0.getAll({ filters });
|
||||
const memories = result.results ?? [];
|
||||
|
||||
if (memories.length === 0) {
|
||||
captureCommandEvent("mem0-tour", { memory_count: 0, scope }, telemetryCtx);
|
||||
pi.sendMessage({ customType: "mem0-tour", content: "No memories found.", display: true });
|
||||
sendFeedback("mem0-tour", `**No memories in ${scope} scope yet** — store one with \`/mem0-remember\`.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const groups = groupByCategory(memories);
|
||||
const lines: string[] = [`**Memory Tour** (${memories.length} total, scope: ${scope})`, ""];
|
||||
const lines: string[] = [
|
||||
`**Memory tour** · ${pluralize(memories.length, "memory", "memories")} · ${scope} scope`,
|
||||
"",
|
||||
];
|
||||
|
||||
for (const [category, items] of groups) {
|
||||
lines.push(`### ${category} (${items.length})`);
|
||||
@@ -148,11 +180,10 @@ export function registerCommands(
|
||||
}
|
||||
|
||||
captureCommandEvent("mem0-tour", { memory_count: memories.length, scope }, telemetryCtx);
|
||||
pi.sendMessage({ customType: "mem0-tour", content: lines.join("\n"), display: true });
|
||||
sendFeedback("mem0-tour", lines.join("\n"));
|
||||
},
|
||||
});
|
||||
|
||||
// ── /mem0-dream ─────────────────────────────────────────────────────
|
||||
pi.registerCommand("mem0-dream", {
|
||||
description: "Consolidate memories — merge duplicates, prune stale entries, resolve contradictions",
|
||||
handler: async (_args, ctx) => {
|
||||
@@ -162,12 +193,14 @@ export function registerCommands(
|
||||
}
|
||||
|
||||
captureCommandEvent("mem0-dream", {}, telemetryCtx);
|
||||
pi.sendMessage({ customType: "mem0-dream", content: DREAM_PROTOCOL, display: true }, { triggerTurn: true });
|
||||
ctx.ui.notify("Dream consolidation started.", "info");
|
||||
pi.sendMessage({ customType: "mem0-dream", content: DREAM_PROTOCOL, display: false }, { triggerTurn: true });
|
||||
sendFeedback(
|
||||
"mem0-dream",
|
||||
"**Dreaming** — reviewing your memories to merge duplicates, resolve contradictions, and prune stale entries. I'll report what changed.",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// ── /mem0-pin ───────────────────────────────────────────────────────
|
||||
pi.registerCommand("mem0-pin", {
|
||||
description: "Pin a memory to protect it from dream pruning",
|
||||
handler: async (args, ctx) => {
|
||||
@@ -177,42 +210,49 @@ export function registerCommands(
|
||||
return;
|
||||
}
|
||||
|
||||
const scopeCtx = getScopeCtx();
|
||||
const filters = resolveSearchFilters(config.defaultScope, scopeCtx);
|
||||
const result = await mem0.search(query, { filters });
|
||||
const memories = result.results ?? [];
|
||||
const memories = await searchMemories(query, config.defaultScope);
|
||||
|
||||
if (memories.length === 0) {
|
||||
captureCommandEvent("mem0-pin", { result_count: 0 }, telemetryCtx);
|
||||
ctx.ui.notify("No matching memories found to pin.", "info");
|
||||
sendFeedback("mem0-pin", `**No matches for "${query}"** — nothing to pin.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pinned = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
|
||||
captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx);
|
||||
sendFeedback(
|
||||
"mem0-pin",
|
||||
["**Pinned** — protected from dream pruning", `- ${formatMemoryCompact(mem)}`].join("\n"),
|
||||
);
|
||||
};
|
||||
const alreadyPinned = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
|
||||
sendFeedback("mem0-pin", ["**Already pinned**", `- ${formatMemoryCompact(mem)}`].join("\n"));
|
||||
};
|
||||
|
||||
if (memories.length === 1) {
|
||||
const target = memories[0];
|
||||
const text = target.memory ?? "";
|
||||
if (text.startsWith("[PINNED]")) {
|
||||
ctx.ui.notify("Already pinned.", "info");
|
||||
alreadyPinned(target);
|
||||
return;
|
||||
}
|
||||
const confirmed = await ctx.ui.confirm(
|
||||
"Pin this memory?",
|
||||
formatMemoryCompact(target),
|
||||
);
|
||||
const confirmed = await ctx.ui.confirm("Pin this memory?", formatMemoryCompact(target));
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Cancelled.", "info");
|
||||
sendFeedback("mem0-pin", "**Cancelled** — nothing was pinned.");
|
||||
return;
|
||||
}
|
||||
await mem0.update(target.id, { text: `[PINNED] ${text}` });
|
||||
captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx);
|
||||
ctx.ui.notify(`Pinned: ${formatMemoryCompact(target)}`, "info");
|
||||
pinned(target);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = memories.map((m) => formatMemoryCompact(m));
|
||||
const selected = await ctx.ui.select("Which memory should I pin?", labels);
|
||||
const selected = await ctx.ui.select(
|
||||
`Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I pin?`,
|
||||
labels,
|
||||
);
|
||||
if (!selected) {
|
||||
ctx.ui.notify("Cancelled.", "info");
|
||||
sendFeedback("mem0-pin", "**Cancelled** — nothing was pinned.");
|
||||
return;
|
||||
}
|
||||
const idx = labels.indexOf(selected);
|
||||
@@ -220,16 +260,14 @@ export function registerCommands(
|
||||
const target = memories[idx];
|
||||
const selectedText = target.memory ?? "";
|
||||
if (selectedText.startsWith("[PINNED]")) {
|
||||
ctx.ui.notify("Already pinned.", "info");
|
||||
alreadyPinned(target);
|
||||
return;
|
||||
}
|
||||
await mem0.update(target.id, { text: `[PINNED] ${selectedText}` });
|
||||
captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx);
|
||||
ctx.ui.notify(`Pinned: ${formatMemoryCompact(target)}`, "info");
|
||||
pinned(target);
|
||||
},
|
||||
});
|
||||
|
||||
// ── /mem0-scope ─────────────────────────────────────────────────────
|
||||
pi.registerCommand("mem0-scope", {
|
||||
description: "Change default memory scope for this session (project, session, global)",
|
||||
handler: async (args, ctx) => {
|
||||
@@ -237,7 +275,13 @@ export function registerCommands(
|
||||
const valid: Scope[] = ["project", "session", "global"];
|
||||
|
||||
if (!scope) {
|
||||
ctx.ui.notify(`Current scope: ${config.defaultScope}. Usage: /mem0-scope <${valid.join("|")}>`, "info");
|
||||
sendFeedback(
|
||||
"mem0-scope",
|
||||
[
|
||||
`**Current scope: ${config.defaultScope}**`,
|
||||
`New memories save to the **${config.defaultScope}** pool. Switch with \`/mem0-scope <${valid.join(" | ")}>\`.`,
|
||||
].join("\n"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -248,11 +292,16 @@ export function registerCommands(
|
||||
|
||||
config.defaultScope = scope as Scope;
|
||||
captureCommandEvent("mem0-scope", { scope }, telemetryCtx);
|
||||
ctx.ui.notify(`Default scope changed to "${scope}" for this session.`, "info");
|
||||
sendFeedback(
|
||||
"mem0-scope",
|
||||
[
|
||||
`**Scope changed to ${scope}**`,
|
||||
`New memories now save to the **${scope}** pool for this session.`,
|
||||
].join("\n"),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// ── /mem0-status ────────────────────────────────────────────────────
|
||||
pi.registerCommand("mem0-status", {
|
||||
description: "Show connection health, identity, project, and memory count",
|
||||
handler: async (_args, _ctx) => {
|
||||
@@ -270,22 +319,21 @@ export function registerCommands(
|
||||
}
|
||||
|
||||
const lines = [
|
||||
"**Mem0 Status**",
|
||||
"**Mem0 status**",
|
||||
"",
|
||||
`- Connection: ${connected ? "connected" : "disconnected"}`,
|
||||
`- User: ${scopeCtx.userId}`,
|
||||
`- Project: ${scopeCtx.appId}`,
|
||||
`- Session: ${scopeCtx.runId}`,
|
||||
`- Default scope: ${config.defaultScope}`,
|
||||
`- Search relevance threshold: ${config.searchThreshold}`,
|
||||
`- Project memories: ${count}`,
|
||||
`- Auto-capture: ${config.autoCapture ? "on" : "off"}`,
|
||||
`- Dream: ${config.dream.enabled ? "enabled" : "disabled"}`,
|
||||
];
|
||||
|
||||
captureCommandEvent("mem0-status", { connected, memory_count: count }, telemetryCtx);
|
||||
pi.sendMessage({ customType: "mem0-status", content: lines.join("\n"), display: true });
|
||||
sendFeedback("mem0-status", lines.join("\n"));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const DEFAULT_CONFIG: Mem0Config = {
|
||||
autoCapture: true,
|
||||
defaultScope: "project",
|
||||
contextInjection: false,
|
||||
searchThreshold: 0.3,
|
||||
dream: DEFAULT_DREAM,
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Mem0Config {
|
||||
autoCapture: boolean;
|
||||
defaultScope: Scope;
|
||||
contextInjection: boolean;
|
||||
searchThreshold: number;
|
||||
dream: DreamConfig;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user