Class: Rubino::CLI::MemoryCommand

Inherits:
Thor
  • Object
show all
Defined in:
lib/rubino/cli/memory_command.rb

Overview

Subcommands for managing persistent memories

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Returns:

  • (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

Raises:

  • (Thor::Error)


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

#listObject



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: options[:kind], limit: options[:limit],
                                include_retired: options[: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

Raises:

  • (Thor::Error)


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