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

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 

9 

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 

27 

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

29 

30_ID_PREVIEW_CHARS = 8 

31 

32 

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) 

39 

40 

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

62 

63 

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) 

99 

100 

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

121 

122 

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)