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