Coverage for src/lilbee/cli/commands/memory.py: 100%
75 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""Local memory commands: add, list, recall, remove."""
3from __future__ import annotations
5from pathlib import Path
7import typer
8from rich.table import Table
10from lilbee.app.memory import (
11 MEMORY_DISABLED_HINT,
12 forget,
13 list_memories,
14 memory_enabled,
15 recall,
16 remember,
17)
18from lilbee.cli.app import (
19 apply_overrides,
20 console,
21 data_dir_option,
22 global_option,
23)
24from lilbee.cli.helpers import json_output
25from lilbee.core.config import cfg
26from lilbee.data.store import MemoryKind
28memory_app = typer.Typer(help="Manage your local long-term memory (off unless enabled).")
30_ID_PREVIEW_CHARS = 8
33def _disabled() -> None:
34 """Report that memory is off, as JSON or a console line."""
35 if cfg.json_mode:
36 json_output({"error": MEMORY_DISABLED_HINT})
37 else:
38 console.print(MEMORY_DISABLED_HINT)
41@memory_app.command(name="add")
42def memory_add(
43 text: str,
44 preference: bool = typer.Option(
45 False, "--preference", "-p", help="Store as an always-recalled preference."
46 ),
47 shared: bool = typer.Option(False, "--shared", help="Also expose this memory to agents."),
48 data_dir: Path | None = data_dir_option,
49 use_global: bool = global_option,
50) -> None:
51 """Remember a fact (or preference) in your local memory."""
52 apply_overrides(data_dir=data_dir, use_global=use_global)
53 if not memory_enabled():
54 _disabled()
55 return
56 kind = MemoryKind.PREFERENCE if preference else MemoryKind.FACT
57 memory_id = remember(text, kind=kind, shared=shared)
58 if cfg.json_mode:
59 json_output({"id": memory_id, "kind": kind.value})
60 return
61 console.print(f"Remembered ({kind.value}).")
64@memory_app.command(name="list")
65def memory_list_cmd(
66 data_dir: Path | None = data_dir_option,
67 use_global: bool = global_option,
68) -> None:
69 """List your stored memories."""
70 apply_overrides(data_dir=data_dir, use_global=use_global)
71 # Listing memories is a pure store read; don't warm the inference fleet.
72 cfg.worker_pool_eager_start = False
73 if not memory_enabled():
74 _disabled()
75 return
76 memories = list_memories()
77 if cfg.json_mode:
78 json_output(
79 {
80 "memories": [
81 {
82 "id": m.id,
83 "kind": m.kind.value,
84 "shared": m.shared,
85 "text": m.text,
86 }
87 for m in memories
88 ]
89 }
90 )
91 return
92 if not memories:
93 console.print("No memories stored.")
94 return
95 table = Table("id", "kind", "shared", "text")
96 for m in memories:
97 table.add_row(m.id[:_ID_PREVIEW_CHARS], m.kind.value, "yes" if m.shared else "no", m.text)
98 console.print(table)
101@memory_app.command(name="recall")
102def memory_recall_cmd(
103 query: str,
104 data_dir: Path | None = data_dir_option,
105 use_global: bool = global_option,
106) -> None:
107 """Recall facts relevant to a query."""
108 apply_overrides(data_dir=data_dir, use_global=use_global)
109 if not memory_enabled():
110 _disabled()
111 return
112 memories = recall(query)
113 if cfg.json_mode:
114 json_output({"memories": [{"id": m.id, "text": m.text} for m in memories]})
115 return
116 if not memories:
117 console.print("No relevant memories.")
118 return
119 for m in memories:
120 console.print(f"- {m.text}")
123@memory_app.command(name="remove")
124def memory_remove(
125 memory_id: str,
126 data_dir: Path | None = data_dir_option,
127 use_global: bool = global_option,
128) -> None:
129 """Delete a memory by id."""
130 apply_overrides(data_dir=data_dir, use_global=use_global)
131 if not memory_enabled():
132 _disabled()
133 return
134 deleted = forget(memory_id)
135 if cfg.json_mode:
136 json_output({"id": memory_id, "deleted": deleted})
137 if not deleted:
138 raise typer.Exit(1)
139 return
140 console.print(f"Removed {memory_id}." if deleted else f"No memory {memory_id} found.")
141 # Exit non-zero on not-found, matching `model remove` / `remove`.
142 if not deleted:
143 raise typer.Exit(1)