Class: Rubino::CLI::MemoryCommand
- Inherits:
-
Thor
- Object
- Thor
- Rubino::CLI::MemoryCommand
- Defined in:
- lib/rubino/cli/memory_command.rb
Overview
Subcommands for managing persistent memories
Class Method Summary collapse
- .exit_on_failure? ⇒ Boolean
-
.render(memory, ui:) ⇒ Object
ONE fact-details rendering for both surfaces (#184): the CLI verb above and the in-chat ‘/memory show <id>` (Commands::Executor).
-
.render_active_backend(ui:) ⇒ Object
ONE backend summary for both surfaces (#184): the CLI ‘memory backend` verb and the in-chat `/memory backend`.
-
.retired_marker(memory) ⇒ Object
‘–all` surfaces soft-retired rows next to live ones; without a flag they were indistinguishable and the supersession chain needed a `show` per id (#161).
-
.safe(text) ⇒ Object
Neutralize terminal-control bytes in untrusted stored text to visible caret/<XX> notation (CWE-150).
Instance Method Summary collapse
- #backend(name = nil) ⇒ Object
- #delete(id) ⇒ Object
- #forget(id) ⇒ Object
- #list ⇒ Object
- #show(id) ⇒ Object
Class Method Details
.exit_on_failure? ⇒ Boolean
12 13 14 |
# File 'lib/rubino/cli/memory_command.rb', line 12 def self.exit_on_failure? true end |
.render(memory, ui:) ⇒ Object
ONE fact-details rendering for both surfaces (#184): the CLI verb above and the in-chat ‘/memory show <id>` (Commands::Executor).
Memory content (and, defensively, every other stored field) is attacker-influenceable — facts are EXTRACTED from conversation, so a raw ‘e]0;…a` / `e[2J` in `content` would hijack the window title or clear the screen the moment `info` printed it (CWE-150, R4-N2). As of #564 PrinterBase#puts_colored (the shared funnel) ALSO defangs every row via sanitize_terminal_keep_sgr — which preserves rubino’s OWN pastel ANSI (the obstacle that previously kept the funnel from sanitizing) while neutralizing the dangerous bytes. These local #safe calls are now belt-and-suspenders (idempotent) but kept so this surface stays safe independent of the funnel.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/rubino/cli/memory_command.rb', line 83 def self.render(memory, ui:) ui.info("ID: #{safe(memory[:id])}") ui.info("Kind: #{safe(memory[:kind])}") ui.info("Confidence: #{safe(memory[:confidence])}") ui.info("Created: #{safe(memory[:created_at])}") # The temporal chain (#88): a soft-retired fact shows when it stopped # being true and which fact replaced it. if memory[:valid_to] ui.info("Retired: #{safe(memory[:valid_to])}") ui.info("Superseded by: #{safe(memory[:superseded_by])}") if memory[:superseded_by] end ui.separator ui.info(safe(memory[:content])) end |
.render_active_backend(ui:) ⇒ Object
ONE backend summary for both surfaces (#184): the CLI ‘memory backend` verb and the in-chat `/memory backend`.
150 151 152 153 154 |
# File 'lib/rubino/cli/memory_command.rb', line 150 def self.render_active_backend(ui:) active = Rubino.configuration.dig("memory", "backend") || Memory::Backends::DEFAULT_NAME ui.info("Active backend: #{active}") ui.info("Available: #{Memory::Backends.names.join(", ")}") end |
.retired_marker(memory) ⇒ Object
‘–all` surfaces soft-retired rows next to live ones; without a flag they were indistinguishable and the supersession chain needed a `show` per id (#161). Marks a tombstone with its retirement date and, when known, the short id of the fact that replaced it. A class method so the in-chat `/memory –all` table (#184) speaks the same dialect.
140 141 142 143 144 145 146 |
# File 'lib/rubino/cli/memory_command.rb', line 140 def self.retired_marker(memory) return "" unless memory[:valid_to] marker = " (retired #{memory[:valid_to][0..9]}" marker += " → #{memory[:superseded_by][0..7]}" if memory[:superseded_by] "#{marker})" end |
.safe(text) ⇒ Object
Neutralize terminal-control bytes in untrusted stored text to visible caret/<XX> notation (CWE-150). Shared by every memory surface that prints a fact field through the non-sanitizing ‘info` funnel.
101 102 103 |
# File 'lib/rubino/cli/memory_command.rb', line 101 def self.safe(text) Util::Output.sanitize_terminal(text) end |
Instance Method Details
#backend(name = nil) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/rubino/cli/memory_command.rb', line 123 def backend(name = nil) return show_backend if name.nil? unless Memory::Backends.registered?(name) raise Thor::Error, "Unknown memory backend: #{name}. Available: #{Memory::Backends.names.join(", ")}" end Config::Writer.new(config_path: config_path).set("memory.backend", name) Rubino.ui.success("memory.backend = #{name}") end |
#delete(id) ⇒ Object
106 107 108 109 110 111 112 |
# File 'lib/rubino/cli/memory_command.rb', line 106 def delete(id) # Same not-found-is-failure contract as #show (P2-H1/H2): exit non-zero # with the error on stderr instead of stdout-printing and returning 0. raise Thor::Error, "memory not found: #{id}" unless backend_store.delete(id) Rubino.ui.success("Memory deleted: #{id}") end |
#forget(id) ⇒ Object
118 119 120 |
# File 'lib/rubino/cli/memory_command.rb', line 118 def forget(id) delete(id) end |
#list ⇒ Object
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/rubino/cli/memory_command.rb', line 25 def list guard_corrupt_database! Rubino.ensure_database_ready! memories = backend_store.list(kind: [:kind], limit: [:limit], include_retired: [:all]) if memories.empty? # Don't dead-end an empty list (#559): point the user at how memories # come to exist (extracted from chat), matching the actionable empty # state `sessions list` gives. With a `--kind` filter active the set may # just be narrowed, so say so; `--all` surfaces superseded facts. hint = if [:kind] "No memories found for kind '#{[:kind]}' (drop --kind to see all)." else "No memories yet — rubino remembers facts from your chats. " \ "Start a `rubino chat` and they'll show up here (use --all for superseded ones)." end Rubino.ui.info(hint) return end rows = memories.map do |m| [m[:id][0..7], m[:kind], "#{m[:content][0..60]}#{self.class.retired_marker(m)}", m[:created_at]] end Rubino.ui.table( headers: %w[ID Kind Content Created], rows: rows ) end |
#show(id) ⇒ Object
58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/rubino/cli/memory_command.rb', line 58 def show(id) memory = backend_store.find(id) # Mirror SessionCommand (#20, P2-H1/H2): a not-found is a FAILURE, so # raise Thor::Error — exit_on_failure? turns it into a non-zero exit with # the message on stderr, so automation can detect the miss and a piped # stdout stays clean. ui.error wrote to stdout and returned 0. raise Thor::Error, "memory not found: #{id}" if memory.nil? self.class.render(memory, ui: Rubino.ui) end |