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

1"""Local memory commands: add, list, recall, remove.""" 

2 

3from __future__ import annotations 

4 

5from pathlib import Path 

6 

7import typer 

8from rich.table import Table 

9from rich.text import Text 

10 

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 

28 

29memory_app = typer.Typer(help="Manage your local long-term memory (off unless enabled).") 

30 

31_ID_PREVIEW_CHARS = 8 

32 

33 

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) 

40 

41 

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}).") 

63 

64 

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) 

102 

103 

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) 

124 

125 

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)