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

Raises:

  • (Thor::Error)


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

#listObject



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: options[:kind], limit: options[:limit],
                                include_retired: options[: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 options[:kind]
        "No memories found for kind '#{options[: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

Raises:

  • (Thor::Error)


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