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
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). The `info`/`success` family does NOT sanitize (PrinterBase#puts_colored is the shared funnel and legitimately receives rubino’s OWN pastel ANSI from other callers, e.g. the ‘/agents` watch view, so it can’t strip escapes wholesale). We therefore neutralize the UNTRUSTED CONTENT here, before it is handed to the printer, into visible caret notation.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/rubino/cli/memory_command.rb', line 71 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`.
130 131 132 133 134 |
# File 'lib/rubino/cli/memory_command.rb', line 130 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.
120 121 122 123 124 125 126 |
# File 'lib/rubino/cli/memory_command.rb', line 120 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.
89 90 91 |
# File 'lib/rubino/cli/memory_command.rb', line 89 def self.safe(text) Util::Output.sanitize_terminal(text) end |
Instance Method Details
#backend(name = nil) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/rubino/cli/memory_command.rb', line 103 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
94 95 96 97 98 99 100 |
# File 'lib/rubino/cli/memory_command.rb', line 94 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 |
#list ⇒ Object
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# 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? Rubino.ui.info("No memories found.") 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
47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/rubino/cli/memory_command.rb', line 47 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 |