swarm_sdk 2.4.3 → 2.4.5

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: 370423fc8b77c4d3cc159123a64795b04bcea24ed6b96090a194329bb1d5af73
4
- data.tar.gz: 05fa10de4f50a64e3bdb919fbea65a4e480cf0eb0b054b2c1e32ef6f5a84c65d
3
+ metadata.gz: a0fcbfaece7a208cfd4ef7afedecda776309b3368ffea0f0800cce3fb1c2ba0f
4
+ data.tar.gz: f41125b70b3e4d83f21132a2231e1ae56f4b2dc35c00c28ef16a51e756fb93b5
5
5
  SHA512:
6
- metadata.gz: 064afcb3024b9a3006ba398f1237f5b7c189ae307a7603ea744312ec885f8ea307b1e9cb186d0b0a9da6b9908c46711b58a50c849dfdefce089ec82c989e5b26
7
- data.tar.gz: eaded0c55ebc089acd20ad14a73a97ae24fe8cd5acf0cc2c557733ac03d224b96291e0717025998b9ba1c703e96465178b1dfab02c563c1d206790729daf3d21
6
+ metadata.gz: 6bc58ad13914191b180912d6ad8d7df187a43a696a5ef6db8f627cd213ab847ac3370d02a4e40e26df9b6bfa882ad3c5aa4adaccdc6c7eab1f3c10179768b97b
7
+ data.tar.gz: 0dcbe3bdbbf6a1443ab33268fd7968c50f874f7de688ca4edce8f7adb53ff3a00ab1910ae363f2b6c07a35401020f8602f3ca3f3ac81f95b6087fd76862e4914
@@ -170,11 +170,16 @@ module SwarmSDK
170
170
 
171
171
  # Fetch real model info for accurate context tracking
172
172
  #
173
+ # Uses SwarmSDK::Models for model lookup (reads from models.json).
174
+ # Falls back to RubyLLM.models if not found in SwarmSDK.
175
+ #
173
176
  # @param model_id [String] Model ID to lookup
174
177
  def fetch_real_model_info(model_id)
175
178
  @model_lookup_error = nil
176
179
  @real_model_info = begin
177
- RubyLLM.models.find(model_id)
180
+ # Try SwarmSDK::Models first (reads from local models.json)
181
+ # Returns ModelInfo object with method access (context_window, etc.)
182
+ SwarmSDK::Models.find(model_id) || RubyLLM.models.find(model_id)
178
183
  rescue StandardError => e
179
184
  suggestions = suggest_similar_models(model_id)
180
185
  @model_lookup_error = {
@@ -74,8 +74,8 @@ module SwarmSDK
74
74
  model_info = SwarmSDK::Models.find(message.model_id)
75
75
  return zero_cost unless model_info
76
76
 
77
- # Extract pricing from SwarmSDK's models.json structure
78
- pricing = model_info["pricing"] || model_info[:pricing]
77
+ # Extract pricing from SwarmSDK's ModelInfo (method access for top-level, Hash for nested)
78
+ pricing = model_info.pricing
79
79
  return zero_cost unless pricing
80
80
 
81
81
  text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
@@ -84,6 +84,35 @@ module SwarmSDK
84
84
  limit - cumulative_total_tokens
85
85
  end
86
86
 
87
+ # Calculate cumulative input cost based on tokens and model pricing
88
+ #
89
+ # @return [Float] Total input cost in dollars
90
+ def cumulative_input_cost
91
+ pricing = model_pricing
92
+ return 0.0 unless pricing
93
+
94
+ input_price = pricing["input_per_million"] || pricing[:input_per_million] || 0.0
95
+ (cumulative_input_tokens / 1_000_000.0) * input_price
96
+ end
97
+
98
+ # Calculate cumulative output cost based on tokens and model pricing
99
+ #
100
+ # @return [Float] Total output cost in dollars
101
+ def cumulative_output_cost
102
+ pricing = model_pricing
103
+ return 0.0 unless pricing
104
+
105
+ output_price = pricing["output_per_million"] || pricing[:output_per_million] || 0.0
106
+ (cumulative_output_tokens / 1_000_000.0) * output_price
107
+ end
108
+
109
+ # Calculate cumulative total cost (input + output)
110
+ #
111
+ # @return [Float] Total cost in dollars
112
+ def cumulative_total_cost
113
+ cumulative_input_cost + cumulative_output_cost
114
+ end
115
+
87
116
  # Compact the conversation history to reduce token usage
88
117
  #
89
118
  # @param options [Hash] Compression options
@@ -92,6 +121,25 @@ module SwarmSDK
92
121
  compactor = ContextCompactor.new(self, options)
93
122
  compactor.compact
94
123
  end
124
+
125
+ private
126
+
127
+ # Get pricing info for the current model
128
+ #
129
+ # Extracts standard text token pricing from model info.
130
+ #
131
+ # @return [Hash, nil] Pricing hash with input_per_million and output_per_million
132
+ def model_pricing
133
+ return unless @real_model_info&.pricing
134
+
135
+ pricing = @real_model_info.pricing
136
+ text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
137
+ return unless text_pricing
138
+
139
+ text_pricing["standard"] || text_pricing[:standard]
140
+ rescue StandardError
141
+ nil
142
+ end
95
143
  end
96
144
  end
97
145
  end
@@ -91,6 +91,7 @@ module SwarmSDK
91
91
  webfetch_base_url: ["SWARM_SDK_WEBFETCH_BASE_URL", nil],
92
92
  webfetch_max_tokens: ["SWARM_SDK_WEBFETCH_MAX_TOKENS", 4096],
93
93
  allow_filesystem_tools: ["SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", true],
94
+ env_interpolation: ["SWARM_SDK_ENV_INTERPOLATION", true],
94
95
  }.freeze
95
96
 
96
97
  class << self
@@ -279,7 +280,7 @@ module SwarmSDK
279
280
  # @return [Integer, Float, Boolean, String] The parsed value
280
281
  def parse_env_value(value, key)
281
282
  case key
282
- when :allow_filesystem_tools
283
+ when :allow_filesystem_tools, :env_interpolation
283
284
  # Convert string to boolean
284
285
  case value.to_s.downcase
285
286
  when "true", "yes", "1", "on", "enabled"
@@ -30,9 +30,18 @@ module SwarmSDK
30
30
  :nodes,
31
31
  :external_swarms
32
32
 
33
- def initialize(yaml_content, base_dir:)
33
+ # Initialize parser with YAML content and options
34
+ #
35
+ # @param yaml_content [String] YAML configuration content
36
+ # @param base_dir [String, Pathname] Base directory for resolving paths
37
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
38
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
39
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
40
+ # When false, skips interpolation entirely.
41
+ def initialize(yaml_content, base_dir:, env_interpolation: nil)
34
42
  @yaml_content = yaml_content
35
43
  @base_dir = Pathname.new(base_dir).expand_path
44
+ @env_interpolation = env_interpolation
36
45
  @config_type = nil
37
46
  @swarm_id = nil
38
47
  @swarm_name = nil
@@ -55,7 +64,7 @@ module SwarmSDK
55
64
  end
56
65
 
57
66
  @config = Utils.symbolize_keys(@config)
58
- interpolate_env_vars!(@config)
67
+ interpolate_env_vars!(@config) if env_interpolation_enabled?
59
68
 
60
69
  validate_version
61
70
  detect_and_validate_type
@@ -86,6 +95,17 @@ module SwarmSDK
86
95
 
87
96
  private
88
97
 
98
+ # Check if environment variable interpolation is enabled
99
+ #
100
+ # Uses the local setting if explicitly set, otherwise falls back to global config.
101
+ #
102
+ # @return [Boolean] true if interpolation should be performed
103
+ def env_interpolation_enabled?
104
+ return @env_interpolation unless @env_interpolation.nil?
105
+
106
+ SwarmSDK.config.env_interpolation
107
+ end
108
+
89
109
  def validate_version
90
110
  version = @config[:version]
91
111
  raise ConfigurationError, "Missing 'version' field in configuration" unless version
@@ -38,9 +38,13 @@ module SwarmSDK
38
38
  # Load configuration from YAML file
39
39
  #
40
40
  # @param path [String, Pathname] Path to YAML configuration file
41
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
42
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
43
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
44
+ # When false, skips interpolation entirely.
41
45
  # @return [Configuration] Validated configuration instance
42
46
  # @raise [ConfigurationError] If file not found or invalid
43
- def load_file(path)
47
+ def load_file(path, env_interpolation: nil)
44
48
  path = Pathname.new(path).expand_path
45
49
 
46
50
  unless path.exist?
@@ -50,7 +54,7 @@ module SwarmSDK
50
54
  yaml_content = File.read(path)
51
55
  base_dir = path.dirname
52
56
 
53
- new(yaml_content, base_dir: base_dir).tap(&:load_and_validate)
57
+ new(yaml_content, base_dir: base_dir, env_interpolation: env_interpolation).tap(&:load_and_validate)
54
58
  rescue Errno::ENOENT
55
59
  raise ConfigurationError, "Configuration file not found: #{path}"
56
60
  end
@@ -60,12 +64,17 @@ module SwarmSDK
60
64
  #
61
65
  # @param yaml_content [String] YAML configuration content
62
66
  # @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
63
- def initialize(yaml_content, base_dir: Dir.pwd)
67
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
68
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
69
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
70
+ # When false, skips interpolation entirely.
71
+ def initialize(yaml_content, base_dir: Dir.pwd, env_interpolation: nil)
64
72
  raise ArgumentError, "yaml_content cannot be nil" if yaml_content.nil?
65
73
  raise ArgumentError, "base_dir cannot be nil" if base_dir.nil?
66
74
 
67
75
  @yaml_content = yaml_content
68
76
  @base_dir = Pathname.new(base_dir).expand_path
77
+ @env_interpolation = env_interpolation
69
78
  @parser = nil
70
79
  @translator = nil
71
80
  end
@@ -77,7 +86,7 @@ module SwarmSDK
77
86
  #
78
87
  # @return [self]
79
88
  def load_and_validate
80
- @parser = Parser.new(@yaml_content, base_dir: @base_dir)
89
+ @parser = Parser.new(@yaml_content, base_dir: @base_dir, env_interpolation: @env_interpolation)
81
90
  @parser.parse
82
91
 
83
92
  # Sync parsed data to instance variables for backward compatibility
@@ -5,7 +5,7 @@
5
5
  "provider": "anthropic",
6
6
  "family": "claude-haiku-4-5",
7
7
  "created_at": null,
8
- "context_window": null,
8
+ "context_window": 200000,
9
9
  "max_output_tokens": 64000,
10
10
  "knowledge_cutoff": null,
11
11
  "modalities": {
@@ -36,7 +36,7 @@
36
36
  "provider": "anthropic",
37
37
  "family": "claude-haiku-4-5",
38
38
  "created_at": null,
39
- "context_window": null,
39
+ "context_window": 200000,
40
40
  "max_output_tokens": 64000,
41
41
  "knowledge_cutoff": null,
42
42
  "modalities": {
@@ -18,16 +18,57 @@ module SwarmSDK
18
18
  MODELS_JSON_PATH = File.expand_path("models.json", __dir__)
19
19
  ALIASES_JSON_PATH = File.expand_path("model_aliases.json", __dir__)
20
20
 
21
+ # Model information wrapper providing method access to model data
22
+ #
23
+ # Wraps the raw Hash from models.json to provide RubyLLM::Model::Info-like
24
+ # interface for compatibility with code expecting method access.
25
+ #
26
+ # @example
27
+ # model = SwarmSDK::Models.find("claude-sonnet-4-5-20250929")
28
+ # model.context_window #=> 200000
29
+ # model.id #=> "claude-sonnet-4-5-20250929"
30
+ class ModelInfo
31
+ attr_reader :id,
32
+ :name,
33
+ :provider,
34
+ :family,
35
+ :context_window,
36
+ :max_output_tokens,
37
+ :knowledge_cutoff,
38
+ :modalities,
39
+ :capabilities,
40
+ :pricing,
41
+ :metadata
42
+
43
+ # Create a ModelInfo from a Hash
44
+ #
45
+ # @param data [Hash] Model data from models.json
46
+ def initialize(data)
47
+ @id = data["id"] || data[:id]
48
+ @name = data["name"] || data[:name]
49
+ @provider = data["provider"] || data[:provider]
50
+ @family = data["family"] || data[:family]
51
+ @context_window = data["context_window"] || data[:context_window]
52
+ @max_output_tokens = data["max_output_tokens"] || data[:max_output_tokens]
53
+ @knowledge_cutoff = data["knowledge_cutoff"] || data[:knowledge_cutoff]
54
+ @modalities = data["modalities"] || data[:modalities]
55
+ @capabilities = data["capabilities"] || data[:capabilities]
56
+ @pricing = data["pricing"] || data[:pricing]
57
+ @metadata = data["metadata"] || data[:metadata]
58
+ end
59
+ end
60
+
21
61
  class << self
22
62
  # Find a model by ID or alias
23
63
  #
24
64
  # @param model_id [String] Model ID or alias to find
25
- # @return [Hash, nil] Model data or nil if not found
65
+ # @return [ModelInfo, nil] Model info or nil if not found
26
66
  def find(model_id)
27
67
  # Check if it's an alias first
28
68
  resolved_id = resolve_alias(model_id)
29
69
 
30
- all.find { |m| m["id"] == resolved_id || m[:id] == resolved_id }
70
+ model_hash = all.find { |m| m["id"] == resolved_id || m[:id] == resolved_id }
71
+ model_hash ? ModelInfo.new(model_hash) : nil
31
72
  end
32
73
 
33
74
  # Resolve a model alias to full model ID
@@ -109,6 +109,58 @@ module SwarmSDK
109
109
  @logs.map { |entry| entry[:agent] }.compact.uniq.map(&:to_sym)
110
110
  end
111
111
 
112
+ # Get per-agent usage breakdown from logs
113
+ #
114
+ # Aggregates context usage, tokens, and cost for each agent from their
115
+ # final agent_stop or agent_step events. Each agent's entry includes:
116
+ # - input_tokens, output_tokens, total_tokens
117
+ # - context_limit, usage_percentage, tokens_remaining
118
+ # - input_cost, output_cost, total_cost
119
+ #
120
+ # @return [Hash{Symbol => Hash}] Per-agent usage breakdown
121
+ #
122
+ # @example
123
+ # result.per_agent_usage[:backend]
124
+ # # => {
125
+ # # input_tokens: 15000,
126
+ # # output_tokens: 5000,
127
+ # # total_tokens: 20000,
128
+ # # context_limit: 200000,
129
+ # # usage_percentage: "10.0%",
130
+ # # tokens_remaining: 180000,
131
+ # # input_cost: 0.045,
132
+ # # output_cost: 0.075,
133
+ # # total_cost: 0.12
134
+ # # }
135
+ def per_agent_usage
136
+ # Find the last usage entry for each agent
137
+ agent_entries = {}
138
+
139
+ @logs.each do |entry|
140
+ next unless entry[:usage] && entry[:agent]
141
+ next unless entry[:type] == "agent_step" || entry[:type] == "agent_stop"
142
+
143
+ agent_name = entry[:agent].to_sym
144
+ agent_entries[agent_name] = entry[:usage]
145
+ end
146
+
147
+ # Build breakdown from final usage entries
148
+ agent_entries.transform_values do |usage|
149
+ {
150
+ input_tokens: usage[:cumulative_input_tokens] || 0,
151
+ output_tokens: usage[:cumulative_output_tokens] || 0,
152
+ total_tokens: usage[:cumulative_total_tokens] || 0,
153
+ cached_tokens: usage[:cumulative_cached_tokens] || 0,
154
+ context_limit: usage[:context_limit],
155
+ usage_percentage: usage[:tokens_used_percentage],
156
+ tokens_remaining: usage[:tokens_remaining],
157
+ input_cost: usage[:input_cost] || 0.0,
158
+ output_cost: usage[:output_cost] || 0.0,
159
+ total_cost: usage[:total_cost] || 0.0,
160
+ }
161
+ end
162
+ end
163
+
112
164
  # Count total LLM requests made
113
165
  # Each LLM API call produces either agent_step (tool calls) or agent_stop (final answer)
114
166
  def llm_requests
@@ -77,6 +77,7 @@ module SwarmSDK
77
77
  total_cost: result.total_cost,
78
78
  total_tokens: result.total_tokens,
79
79
  agents_involved: result.agents_involved,
80
+ per_agent_usage: result.per_agent_usage,
80
81
  result: result,
81
82
  timestamp: Time.now.utc.iso8601,
82
83
  },
@@ -208,6 +208,7 @@ module SwarmSDK
208
208
  total_cost: context.metadata[:total_cost],
209
209
  total_tokens: context.metadata[:total_tokens],
210
210
  agents_involved: context.metadata[:agents_involved],
211
+ per_agent_usage: context.metadata[:per_agent_usage],
211
212
  timestamp: context.metadata[:timestamp],
212
213
  )
213
214
  end
@@ -366,6 +366,47 @@ module SwarmSDK
366
366
  @agent_definitions.keys
367
367
  end
368
368
 
369
+ # Get context usage breakdown for all agents
370
+ #
371
+ # Returns per-agent context statistics including tokens used, context limit,
372
+ # usage percentage, and cost. Useful for monitoring context window consumption
373
+ # across the swarm.
374
+ #
375
+ # @return [Hash{Symbol => Hash}] Per-agent context breakdown
376
+ #
377
+ # @example
378
+ # breakdown = swarm.context_breakdown
379
+ # breakdown[:backend]
380
+ # # => {
381
+ # # input_tokens: 15000,
382
+ # # output_tokens: 5000,
383
+ # # total_tokens: 20000,
384
+ # # cached_tokens: 2000,
385
+ # # context_limit: 200000,
386
+ # # usage_percentage: 10.0,
387
+ # # tokens_remaining: 180000,
388
+ # # input_cost: 0.045,
389
+ # # output_cost: 0.075,
390
+ # # total_cost: 0.12
391
+ # # }
392
+ def context_breakdown
393
+ initialize_agents unless @agents_initialized
394
+
395
+ breakdown = {}
396
+
397
+ # Include primary agents
398
+ @agents.each do |name, chat|
399
+ breakdown[name] = build_agent_context_info(chat)
400
+ end
401
+
402
+ # Include delegation instances
403
+ @delegation_instances.each do |instance_name, chat|
404
+ breakdown[instance_name.to_sym] = build_agent_context_info(chat)
405
+ end
406
+
407
+ breakdown
408
+ end
409
+
369
410
  # Implement Snapshotable interface
370
411
  def primary_agents
371
412
  @agents
@@ -546,6 +587,29 @@ module SwarmSDK
546
587
  end
547
588
  end
548
589
 
590
+ # Build context info hash for an agent chat instance
591
+ #
592
+ # @param chat [Agent::Chat] Agent chat instance with TokenTracking
593
+ # @return [Hash] Context usage information
594
+ def build_agent_context_info(chat)
595
+ return {} unless chat.respond_to?(:cumulative_input_tokens)
596
+
597
+ {
598
+ input_tokens: chat.cumulative_input_tokens,
599
+ output_tokens: chat.cumulative_output_tokens,
600
+ total_tokens: chat.cumulative_total_tokens,
601
+ cached_tokens: chat.cumulative_cached_tokens,
602
+ cache_creation_tokens: chat.cumulative_cache_creation_tokens,
603
+ effective_input_tokens: chat.effective_input_tokens,
604
+ context_limit: chat.context_limit,
605
+ usage_percentage: chat.context_usage_percentage,
606
+ tokens_remaining: chat.tokens_remaining,
607
+ input_cost: chat.cumulative_input_cost,
608
+ output_cost: chat.cumulative_output_cost,
609
+ total_cost: chat.cumulative_total_cost,
610
+ }
611
+ end
612
+
549
613
  # Validate that observer agent exists
550
614
  #
551
615
  # @param agent_name [Symbol] Name of the observer agent
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.4.3"
4
+ VERSION = "2.4.5"
5
5
  end
data/lib/swarm_sdk.rb CHANGED
@@ -275,6 +275,11 @@ module SwarmSDK
275
275
  #
276
276
  # @param yaml_content [String] YAML configuration content
277
277
  # @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
278
+ # @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
279
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
280
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
281
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
282
+ # When false, skips interpolation entirely.
278
283
  # @return [Swarm, Workflow] Configured swarm or workflow instance
279
284
  # @raise [ConfigurationError] If YAML is invalid or configuration is incorrect
280
285
  #
@@ -297,8 +302,11 @@ module SwarmSDK
297
302
  # @example Load with default base_dir (Dir.pwd)
298
303
  # yaml = File.read("config.yml")
299
304
  # swarm = SwarmSDK.load(yaml) # base_dir defaults to Dir.pwd
300
- def load(yaml_content, base_dir: Dir.pwd, allow_filesystem_tools: nil)
301
- config = Configuration.new(yaml_content, base_dir: base_dir)
305
+ #
306
+ # @example Load without environment variable interpolation
307
+ # swarm = SwarmSDK.load(yaml, env_interpolation: false)
308
+ def load(yaml_content, base_dir: Dir.pwd, allow_filesystem_tools: nil, env_interpolation: nil)
309
+ config = Configuration.new(yaml_content, base_dir: base_dir, env_interpolation: env_interpolation)
302
310
  config.load_and_validate
303
311
  swarm = config.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
304
312
 
@@ -320,6 +328,11 @@ module SwarmSDK
320
328
  # loading swarms from configuration files.
321
329
  #
322
330
  # @param path [String, Pathname] Path to YAML configuration file
331
+ # @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
332
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
333
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
334
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
335
+ # When false, skips interpolation entirely.
323
336
  # @return [Swarm, Workflow] Configured swarm or workflow instance
324
337
  # @raise [ConfigurationError] If file not found or configuration invalid
325
338
  #
@@ -329,8 +342,11 @@ module SwarmSDK
329
342
  #
330
343
  # @example With absolute path
331
344
  # swarm = SwarmSDK.load_file("/absolute/path/config.yml")
332
- def load_file(path, allow_filesystem_tools: nil)
333
- config = Configuration.load_file(path)
345
+ #
346
+ # @example Load without environment variable interpolation
347
+ # swarm = SwarmSDK.load_file("config.yml", env_interpolation: false)
348
+ def load_file(path, allow_filesystem_tools: nil, env_interpolation: nil)
349
+ config = Configuration.load_file(path, env_interpolation: env_interpolation)
334
350
  swarm = config.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
335
351
 
336
352
  # Apply hooks if any are configured (YAML-only feature)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.3
4
+ version: 2.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda