legion-tty 0.4.13 → 0.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc958211b7004e0392a9e96cf2a874d6c54d6f0c6f183d87d80f428e16d3b014
4
- data.tar.gz: 13264f1761391724054983f250767d9308aab1a397d0dc1577292268a0f7e031
3
+ metadata.gz: 22a1a4b5a0dcae6da430188229f80fc7dfbbdbbc75c7b58516d93d7b35ee60cc
4
+ data.tar.gz: 309f256644b595a3f1aa15501482d40b5c27b26aa18707fd237207c479f14946
5
5
  SHA512:
6
- metadata.gz: fd92069e863ebb5bd1185068adf3f50323fb276a2ae0479a8253e0220c1e4bc22a76abb7b8eb73a4c4a22c84be9f22cddf6eea22d510d7772fd459d3d74a1ff9
7
- data.tar.gz: afcce634192c2e16f133b0d237cd3720536dfce07e2cae17089f208867546d82d75a58bb352887da1d0d29c0e651d047b79596fea0428d35f152e48f17707069
6
+ metadata.gz: d2fe62fbe7805f0874f1988c7b1d226c00304bcfd86c84011b6e5a95707e684170d91966d1947360ed3ce021328ed3e57f445bb2b85cc5463891225b58f796a0
7
+ data.tar.gz: a6cf5433e3a80fa06f72003ae079d8cdf3e1b30cc1adaa64555c78becb3fc1a530889c9aacf3c031e988ff55c28ef8463e37e89196d713d6b9c87e238698871c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.15] - 2026-03-19
4
+
5
+ ### Added
6
+ - `/autosave [N|off]` command: toggle periodic auto-save with configurable interval (default 60s)
7
+ - `/react <emoji>` command: add emoji reactions to messages (displayed in render)
8
+ - `/macro record|stop|play|list|delete` command: record and replay slash command sequences
9
+ - `/tag` and `/tags` commands: tag messages with labels, filter by tag, show tag statistics
10
+
11
+ ## [0.4.14] - 2026-03-19
12
+
13
+ ### Added
14
+ - LLM response timing: tracks elapsed time per response, shows notification, includes avg in `/stats`
15
+ - `/wc` command: word count statistics per role (user/assistant/system) with averages
16
+ - `/import <path>` command: import session from any JSON file path with validation
17
+ - `/mute` command: toggle system message display in chat (messages still tracked, just hidden)
18
+
3
19
  ## [0.4.13] - 2026-03-19
4
20
 
5
21
  ### Added
@@ -5,12 +5,15 @@ require_relative '../theme'
5
5
  module Legion
6
6
  module TTY
7
7
  module Components
8
+ # rubocop:disable Metrics/ClassLength
8
9
  class MessageStream
9
10
  attr_reader :messages, :scroll_offset
11
+ attr_accessor :mute_system
10
12
 
11
13
  def initialize
12
14
  @messages = []
13
15
  @scroll_offset = 0
16
+ @mute_system = false
14
17
  end
15
18
 
16
19
  def add_message(role:, content:)
@@ -69,7 +72,11 @@ module Legion
69
72
  private
70
73
 
71
74
  def build_all_lines(width)
72
- @messages.flat_map { |msg| render_message(msg, width) }
75
+ @messages.flat_map do |msg|
76
+ next [] if @mute_system && msg[:role] == :system
77
+
78
+ render_message(msg, width)
79
+ end
73
80
  end
74
81
 
75
82
  def render_message(msg, width)
@@ -89,7 +96,9 @@ module Legion
89
96
  def user_lines(msg, _width)
90
97
  ts = format_timestamp(msg[:timestamp])
91
98
  header = "#{Theme.c(:accent, 'You')} #{Theme.c(:muted, ts)}"
92
- ['', "#{header}: #{msg[:content]}"]
99
+ lines = ['', "#{header}: #{msg[:content]}"]
100
+ lines << reaction_line(msg) if msg[:reactions]&.any?
101
+ lines
93
102
  end
94
103
 
95
104
  def format_timestamp(time)
@@ -100,7 +109,14 @@ module Legion
100
109
 
101
110
  def assistant_lines(msg, width)
102
111
  rendered = render_markdown(msg[:content], width)
103
- ['', *rendered.split("\n")]
112
+ lines = ['', *rendered.split("\n")]
113
+ lines << reaction_line(msg) if msg[:reactions]&.any?
114
+ lines
115
+ end
116
+
117
+ def reaction_line(msg)
118
+ reactions = msg[:reactions].map { |r| "[#{r}]" }.join(' ')
119
+ " #{Theme.c(:muted, reactions)}"
104
120
  end
105
121
 
106
122
  def render_markdown(text, width)
@@ -131,6 +147,7 @@ module Legion
131
147
  panel.instance_variable_set(:@error, error) if error
132
148
  end
133
149
  end
150
+ # rubocop:enable Metrics/ClassLength
134
151
  end
135
152
  end
136
153
  end
@@ -125,6 +125,105 @@ module Legion
125
125
  end
126
126
  # rubocop:enable Metrics/AbcSize
127
127
 
128
+ # rubocop:disable Metrics/MethodLength
129
+ def handle_macro(input)
130
+ parts = input.split(nil, 3)
131
+ subcommand = parts[1]
132
+ name = parts[2]
133
+
134
+ case subcommand
135
+ when 'record'
136
+ macro_record(name)
137
+ when 'stop'
138
+ macro_stop
139
+ when 'play'
140
+ macro_play(name)
141
+ when 'list'
142
+ macro_list
143
+ when 'delete'
144
+ macro_delete(name)
145
+ else
146
+ @message_stream.add_message(
147
+ role: :system,
148
+ content: 'Usage: /macro record|stop|play|list|delete <name>'
149
+ )
150
+ end
151
+ :handled
152
+ end
153
+ # rubocop:enable Metrics/MethodLength
154
+
155
+ def macro_record(name)
156
+ unless name
157
+ @message_stream.add_message(role: :system, content: 'Usage: /macro record <name>')
158
+ return
159
+ end
160
+
161
+ @recording_macro = name
162
+ @macro_buffer = []
163
+ @message_stream.add_message(role: :system,
164
+ content: "Recording macro '#{name}'... Use /macro stop to finish.")
165
+ end
166
+
167
+ def macro_stop
168
+ unless @recording_macro
169
+ @message_stream.add_message(role: :system, content: 'No macro recording in progress.')
170
+ return
171
+ end
172
+
173
+ name = @recording_macro
174
+ @macros[name] = @macro_buffer.dup
175
+ @recording_macro = nil
176
+ @macro_buffer = []
177
+ @message_stream.add_message(role: :system,
178
+ content: "Macro '#{name}' saved (#{@macros[name].size} commands).")
179
+ end
180
+
181
+ def macro_play(name)
182
+ unless name
183
+ @message_stream.add_message(role: :system, content: 'Usage: /macro play <name>')
184
+ return
185
+ end
186
+
187
+ commands = @macros[name]
188
+ unless commands
189
+ @message_stream.add_message(role: :system, content: "Macro '#{name}' not found.")
190
+ return
191
+ end
192
+
193
+ @message_stream.add_message(role: :system,
194
+ content: "Playing macro '#{name}' (#{commands.size} commands)...")
195
+ commands.each { |cmd| handle_slash_command(cmd) }
196
+ end
197
+
198
+ def macro_list
199
+ if @macros.empty?
200
+ @message_stream.add_message(role: :system, content: 'No macros defined.')
201
+ return
202
+ end
203
+
204
+ lines = @macros.map do |n, cmds|
205
+ preview = cmds.first(3).join(', ')
206
+ preview += ', ...' if cmds.size > 3
207
+ " #{n} (#{cmds.size}): #{preview}"
208
+ end
209
+ status = @recording_macro ? " [recording: #{@recording_macro}]" : ''
210
+ @message_stream.add_message(role: :system,
211
+ content: "Macros (#{@macros.size})#{status}:\n#{lines.join("\n")}")
212
+ end
213
+
214
+ def macro_delete(name)
215
+ unless name
216
+ @message_stream.add_message(role: :system, content: 'Usage: /macro delete <name>')
217
+ return
218
+ end
219
+
220
+ if @macros.delete(name)
221
+ @message_stream.add_message(role: :system, content: "Macro '#{name}' deleted.")
222
+ else
223
+ @message_stream.add_message(role: :system, content: "Macro '#{name}' not found.")
224
+ end
225
+ end
226
+
128
227
  def snippet_delete(name)
129
228
  unless name
130
229
  @message_stream.add_message(role: :system, content: 'Usage: /snippet delete <name>')
@@ -162,6 +162,70 @@ module Legion
162
162
  :handled
163
163
  end
164
164
 
165
+ # rubocop:disable Metrics/AbcSize
166
+ def handle_react(input)
167
+ parts = input.split(nil, 3)
168
+ if parts.size == 2
169
+ emoji = parts[1]
170
+ msg = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
171
+ elsif parts.size >= 3 && parts[1].match?(/\A\d+\z/)
172
+ idx = parts[1].to_i
173
+ emoji = parts[2]
174
+ msg = @message_stream.messages[idx]
175
+ else
176
+ @message_stream.add_message(role: :system, content: 'Usage: /react <emoji> or /react <N> <emoji>')
177
+ return :handled
178
+ end
179
+
180
+ unless msg
181
+ @message_stream.add_message(role: :system, content: 'No message to react to.')
182
+ return :handled
183
+ end
184
+
185
+ msg[:reactions] ||= []
186
+ msg[:reactions] << emoji
187
+ @message_stream.add_message(role: :system, content: "Reaction #{emoji} added.")
188
+ :handled
189
+ end
190
+ # rubocop:enable Metrics/AbcSize
191
+
192
+ # rubocop:disable Metrics/AbcSize
193
+ def handle_tag(input)
194
+ parts = input.split(nil, 3)
195
+ if parts.size == 2
196
+ label = parts[1]
197
+ msg = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
198
+ elsif parts.size >= 3 && parts[1].match?(/\A\d+\z/)
199
+ idx = parts[1].to_i
200
+ label = parts[2]
201
+ msg = @message_stream.messages[idx]
202
+ else
203
+ @message_stream.add_message(role: :system, content: 'Usage: /tag <label> or /tag <N> <label>')
204
+ return :handled
205
+ end
206
+
207
+ unless msg
208
+ @message_stream.add_message(role: :system, content: 'No message to tag.')
209
+ return :handled
210
+ end
211
+
212
+ msg[:tags] ||= []
213
+ msg[:tags] |= [label]
214
+ @message_stream.add_message(role: :system, content: "Tag '#{label}' added.")
215
+ :handled
216
+ end
217
+ # rubocop:enable Metrics/AbcSize
218
+
219
+ def handle_tags(input)
220
+ label = input.split(nil, 2)[1]
221
+ if label
222
+ filter_messages_by_tag(label)
223
+ else
224
+ show_all_tags
225
+ end
226
+ :handled
227
+ end
228
+
165
229
  def search_messages(query)
166
230
  pattern = query.downcase
167
231
  @message_stream.messages.select do |msg|
@@ -184,6 +248,34 @@ module Legion
184
248
  nil
185
249
  end
186
250
  end
251
+
252
+ # rubocop:disable Metrics/AbcSize
253
+ def show_all_tags
254
+ tagged = @message_stream.messages.select { |m| m[:tags]&.any? }
255
+ if tagged.empty?
256
+ @message_stream.add_message(role: :system, content: 'No tagged messages.')
257
+ return
258
+ end
259
+
260
+ counts = Hash.new(0)
261
+ tagged.each { |m| m[:tags].each { |t| counts[t] += 1 } }
262
+ lines = counts.sort.map { |tag, count| " ##{tag} (#{count})" }
263
+ @message_stream.add_message(role: :system, content: "Tags:\n#{lines.join("\n")}")
264
+ end
265
+ # rubocop:enable Metrics/AbcSize
266
+
267
+ def filter_messages_by_tag(label)
268
+ results = @message_stream.messages.select { |m| m[:tags]&.include?(label) }
269
+ if results.empty?
270
+ @message_stream.add_message(role: :system, content: "No messages tagged '##{label}'.")
271
+ else
272
+ lines = results.map { |r| " [#{r[:role]}] #{truncate_text(r[:content].to_s, 80)}" }
273
+ @message_stream.add_message(
274
+ role: :system,
275
+ content: "Messages tagged '##{label}' (#{results.size}):\n#{lines.join("\n")}"
276
+ )
277
+ end
278
+ end
187
279
  end
188
280
  # rubocop:enable Metrics/ModuleLength
189
281
  end
@@ -4,6 +4,7 @@ module Legion
4
4
  module TTY
5
5
  module Screens
6
6
  class Chat < Base
7
+ # rubocop:disable Metrics/ModuleLength
7
8
  module SessionCommands
8
9
  private
9
10
 
@@ -76,6 +77,75 @@ module Legion
76
77
  :handled
77
78
  end
78
79
 
80
+ def handle_import(input)
81
+ path = input.split(nil, 2)[1]
82
+ unless path
83
+ @message_stream.add_message(role: :system, content: 'Usage: /import <path>')
84
+ return :handled
85
+ end
86
+
87
+ load_import_file(File.expand_path(path))
88
+ rescue ::JSON::ParserError => e
89
+ @message_stream.add_message(role: :system, content: "Invalid JSON: #{e.message}")
90
+ :handled
91
+ end
92
+
93
+ def load_import_file(path)
94
+ unless File.exist?(path)
95
+ @message_stream.add_message(role: :system, content: "File not found: #{path}")
96
+ return :handled
97
+ end
98
+
99
+ data = ::JSON.parse(File.read(path), symbolize_names: true)
100
+ unless data.is_a?(Hash) && data[:messages].is_a?(Array)
101
+ @message_stream.add_message(role: :system, content: 'Invalid session file: missing messages array.')
102
+ return :handled
103
+ end
104
+
105
+ apply_imported_messages(data[:messages], path)
106
+ end
107
+
108
+ def apply_imported_messages(messages, path)
109
+ imported = messages.map { |m| { role: m[:role].to_sym, content: m[:content].to_s } }
110
+ @message_stream.messages.replace(imported)
111
+ @status_bar.notify(message: "Imported #{imported.size} messages", level: :success, ttl: 3)
112
+ @message_stream.add_message(role: :system, content: "Imported #{imported.size} messages from #{path}.")
113
+ :handled
114
+ end
115
+
116
+ def handle_autosave(input)
117
+ arg = input.split(nil, 2)[1]
118
+ if arg.nil?
119
+ @autosave_enabled = !@autosave_enabled
120
+ status = @autosave_enabled ? "ON (every #{@autosave_interval}s)" : 'OFF'
121
+ @status_bar.notify(message: "Autosave: #{status}", level: :info, ttl: 3)
122
+ @message_stream.add_message(role: :system, content: "Autosave #{status}.")
123
+ elsif arg == 'off'
124
+ @autosave_enabled = false
125
+ @status_bar.notify(message: 'Autosave: OFF', level: :info, ttl: 3)
126
+ @message_stream.add_message(role: :system, content: 'Autosave OFF.')
127
+ elsif arg.match?(/\A\d+\z/)
128
+ @autosave_interval = arg.to_i
129
+ @autosave_enabled = true
130
+ @status_bar.notify(message: "Autosave: ON (every #{@autosave_interval}s)", level: :info, ttl: 3)
131
+ @message_stream.add_message(role: :system, content: "Autosave ON (every #{@autosave_interval}s).")
132
+ else
133
+ @message_stream.add_message(role: :system, content: 'Usage: /autosave [off|<seconds>]')
134
+ end
135
+ :handled
136
+ end
137
+
138
+ def check_autosave
139
+ return unless @autosave_enabled
140
+ return unless Time.now - @last_autosave >= @autosave_interval
141
+
142
+ auto_save_session
143
+ @last_autosave = Time.now
144
+ @status_bar.notify(message: 'Autosaved', level: :info, ttl: 2)
145
+ rescue StandardError
146
+ nil
147
+ end
148
+
79
149
  def auto_save_session
80
150
  return if @message_stream.messages.empty?
81
151
 
@@ -87,6 +157,7 @@ module Legion
87
157
  nil
88
158
  end
89
159
  end
160
+ # rubocop:enable Metrics/ModuleLength
90
161
  end
91
162
  end
92
163
  end
@@ -204,15 +204,62 @@ module Legion
204
204
  end
205
205
  end
206
206
 
207
+ def handle_wc
208
+ msgs = @message_stream.messages
209
+ by_role = word_counts_by_role(msgs)
210
+ total = by_role.values.sum
211
+ avg = (total.to_f / [msgs.size, 1].max).round
212
+ @message_stream.add_message(role: :system, content: build_wc_lines(by_role, total, avg).join("\n"))
213
+ :handled
214
+ end
215
+
216
+ def word_counts_by_role(msgs)
217
+ %i[user assistant system].to_h do |role|
218
+ words = msgs.select { |m| m[:role] == role }.sum { |m| m[:content].to_s.split.size }
219
+ [role, words]
220
+ end
221
+ end
222
+
223
+ def build_wc_lines(by_role, total, avg)
224
+ [
225
+ 'Word count:',
226
+ " Total: #{format_stat_number(total)}",
227
+ " User: #{format_stat_number(by_role[:user])}",
228
+ " Assistant: #{format_stat_number(by_role[:assistant])}",
229
+ " System: #{format_stat_number(by_role[:system])}",
230
+ " Avg words/message: #{avg}"
231
+ ]
232
+ end
233
+
234
+ def handle_mute
235
+ @muted_system = !@muted_system
236
+ @message_stream.mute_system = @muted_system
237
+ if @muted_system
238
+ @status_bar.notify(message: 'System messages hidden', level: :info, ttl: 3)
239
+ else
240
+ @status_bar.notify(message: 'System messages visible', level: :info, ttl: 3)
241
+ end
242
+ :handled
243
+ end
244
+
207
245
  def build_stats_lines
208
246
  msgs = @message_stream.messages
209
247
  counts = count_by_role(msgs)
210
248
  total_chars = msgs.sum { |m| m[:content].to_s.length }
211
249
  lines = stats_header_lines(msgs, counts, total_chars)
212
250
  lines << " Tool calls: #{counts[:tool]}" if counts[:tool].positive?
251
+ append_response_time_stat(lines, msgs)
213
252
  lines
214
253
  end
215
254
 
255
+ def append_response_time_stat(lines, msgs)
256
+ timed = msgs.select { |m| m[:response_time] }
257
+ return unless timed.any?
258
+
259
+ avg_rt = timed.sum { |m| m[:response_time] }.to_f / timed.size
260
+ lines << " Avg response time: #{avg_rt.round(2)}s (#{timed.size} responses)"
261
+ end
262
+
216
263
  def count_by_role(msgs)
217
264
  %i[user assistant system tool].to_h { |role| [role, msgs.count { |m| m[:role] == role }] }
218
265
  end
@@ -28,7 +28,8 @@ module Legion
28
28
  SLASH_COMMANDS = %w[/help /quit /clear /compact /copy /diff /model /session /cost /export /tools /dashboard
29
29
  /hotkeys /save /load /sessions /system /delete /plan /palette /extensions /config
30
30
  /theme /search /grep /stats /personality /undo /history /pin /pins /rename
31
- /context /alias /snippet /debug /uptime /time /bookmark /welcome /tips].freeze
31
+ /context /alias /snippet /debug /uptime /time /bookmark /welcome /tips
32
+ /wc /import /mute /autosave /react /macro /tag /tags].freeze
32
33
 
33
34
  PERSONALITIES = {
34
35
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -40,7 +41,7 @@ module Legion
40
41
 
41
42
  attr_reader :message_stream, :status_bar
42
43
 
43
- # rubocop:disable Metrics/AbcSize
44
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
44
45
  def initialize(app, output: $stdout, input_bar: nil)
45
46
  super(app)
46
47
  @output = output
@@ -56,11 +57,18 @@ module Legion
56
57
  @pinned_messages = []
57
58
  @aliases = {}
58
59
  @snippets = {}
60
+ @macros = {}
59
61
  @debug_mode = false
60
62
  @session_start = Time.now
63
+ @muted_system = false
64
+ @autosave_enabled = false
65
+ @autosave_interval = 60
66
+ @last_autosave = Time.now
67
+ @recording_macro = nil
68
+ @macro_buffer = []
61
69
  end
62
70
 
63
- # rubocop:enable Metrics/AbcSize
71
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
64
72
 
65
73
  def activate
66
74
  @running = true
@@ -114,7 +122,9 @@ module Legion
114
122
  return handle_slash_command("#{expanded} #{input.split(nil, 2)[1]}".strip)
115
123
  end
116
124
 
117
- dispatch_slash(cmd, input)
125
+ result = dispatch_slash(cmd, input)
126
+ record_macro_step(input, cmd, result)
127
+ result
118
128
  end
119
129
 
120
130
  def handle_user_message(input)
@@ -126,6 +136,7 @@ module Legion
126
136
  send_to_llm(input)
127
137
  end
128
138
  @status_bar.update(message_count: @message_stream.messages.size)
139
+ check_autosave
129
140
  render_screen
130
141
  end
131
142
 
@@ -173,6 +184,14 @@ module Legion
173
184
 
174
185
  private
175
186
 
187
+ def record_macro_step(input, cmd, result)
188
+ return unless @recording_macro
189
+ return if cmd == '/macro'
190
+ return unless result == :handled
191
+
192
+ @macro_buffer << input
193
+ end
194
+
176
195
  def setup_system_prompt
177
196
  cfg = safe_config
178
197
  return unless @llm_chat && cfg.is_a?(Hash) && !cfg.empty?
@@ -202,15 +221,23 @@ module Legion
202
221
 
203
222
  @status_bar.update(thinking: true)
204
223
  render_screen
224
+ start_time = Time.now
205
225
  response = @llm_chat.ask(message) do |chunk|
206
226
  @status_bar.update(thinking: false)
207
227
  @message_stream.append_streaming(chunk.content) if chunk.content
208
228
  render_screen
209
229
  end
230
+ record_response_time(Time.now - start_time)
210
231
  @status_bar.update(thinking: false)
211
232
  track_response_tokens(response)
212
233
  end
213
234
 
235
+ def record_response_time(elapsed)
236
+ @last_response_time = elapsed
237
+ @message_stream.messages.last[:response_time] = elapsed if @message_stream.messages.last
238
+ @status_bar.notify(message: "Response: #{elapsed.round(1)}s", level: :info, ttl: 4)
239
+ end
240
+
214
241
  def daemon_available?
215
242
  !!(defined?(Legion::LLM::DaemonClient) && Legion::LLM::DaemonClient.available?)
216
243
  end
@@ -340,6 +367,14 @@ module Legion
340
367
  when '/bookmark' then handle_bookmark
341
368
  when '/welcome' then handle_welcome
342
369
  when '/tips' then handle_tips
370
+ when '/wc' then handle_wc
371
+ when '/import' then handle_import(input)
372
+ when '/mute' then handle_mute
373
+ when '/autosave' then handle_autosave(input)
374
+ when '/react' then handle_react(input)
375
+ when '/macro' then handle_macro(input)
376
+ when '/tag' then handle_tag(input)
377
+ when '/tags' then handle_tags(input)
343
378
  else :handled
344
379
  end
345
380
  end
@@ -418,7 +453,9 @@ module Legion
418
453
  "personality:#{@personality || 'default'} " \
419
454
  "aliases:#{@aliases.size} " \
420
455
  "snippets:#{@snippets.size} " \
421
- "pinned:#{@pinned_messages.size}"
456
+ "macros:#{@macros.size} " \
457
+ "pinned:#{@pinned_messages.size} " \
458
+ "autosave:#{@autosave_enabled}"
422
459
  end
423
460
 
424
461
  def build_default_input_bar
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.13'
5
+ VERSION = '0.4.15'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-tty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.13
4
+ version: 0.4.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity