cline-rb 2.2.0 → 2.3.0

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: 8d691efd743f7f5ed4826457a94e44d087daebb4545afad5b17b775b0fa86419
4
- data.tar.gz: 49c1abc4ee95dced54956e00f30b75dc1886cf8a1e3ec35f18ff488b8972e6c1
3
+ metadata.gz: 5df017c7913b29844cc5cdc1611e355bcee832d6f75c09d0765a88eb9ff91b3a
4
+ data.tar.gz: 355b9674891ed24101c3f44d5087edf7060f33f85a8a4b010aedca55014d03b5
5
5
  SHA512:
6
- metadata.gz: bec4f499ba41f796dd1e1e9f6292811854da30e9a7f8ebcf2fa9f8a87273a85908956f02ba41aba3fd9402f810c90234085690a1f7df4c46f54d8b9c7733fcf3
7
- data.tar.gz: 7896bbb8e74e4efde6ea41a725df53f0e83909a2f3f7845ea86674905b43101c75452a4a78f2b2cac7ce020a4bf14f3528078fea02abda2009ea6e00523d96c3
6
+ metadata.gz: 918c2580895ea328088cf37bd92214c510417a7c457d6fc12e2826a807eaedb6800ae1b24c87064c7103defa61fe9d8ca042f1e8aae0ff0a2aad9bf680167c4e
7
+ data.tar.gz: bd0abcf30c86ed77861df47ab3c46d6ccee9348ff17b5662c431ae9c515f69478f863367e17baf7a12f63535585f246d975c1d87f55cf97e9c5aed0f000ae17f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ # [v2.3.0](https://github.com/Muriel-Salvan/cline-rb/compare/v2.2.0...v2.3.0) (2026-11-04 15:21:27)
2
+
3
+ ## Global changes
4
+ ### Patches
5
+
6
+ * [feat(cline): add LenientSchema for tolerant deserialization of session messages's input](https://github.com/Muriel-Salvan/cline-rb/commit/a033d62989c22876030742c5e4979dc7a02bee16)
7
+
8
+ ## Changes for cline
9
+ ### Features
10
+
11
+ * [feat(cline): add LenientSchema for tolerant deserialization of session messages's input](https://github.com/Muriel-Salvan/cline-rb/commit/a033d62989c22876030742c5e4979dc7a02bee16)
12
+
13
+ ## Changes for schema
14
+ ### Features
15
+
16
+ * [feat(cline): add LenientSchema for tolerant deserialization of session messages's input](https://github.com/Muriel-Salvan/cline-rb/commit/a033d62989c22876030742c5e4979dc7a02bee16)
17
+
1
18
  # [v2.2.0](https://github.com/Muriel-Salvan/cline-rb/compare/v2.1.1...v2.2.0) (2026-11-03 16:44:07)
2
19
 
3
20
  ## Global changes
data/README.md CHANGED
@@ -592,7 +592,7 @@ msg.usage #=> Usage struct (cost, tokens)
592
592
  msg.to_human #=> human-friendly string (limited to 128 chars by default)
593
593
  ```
594
594
 
595
- Contains nested schemas: `ModelInfo`, `Metrics`, `MessageContent`, `ToolUseInput`.
595
+ Contains nested schemas: `ModelInfo`, `Metrics`, `MessageContent`, `ToolUseInput`, `ToolResultContent`.
596
596
 
597
597
  ---
598
598
 
@@ -877,6 +877,7 @@ Every domain object (settings, providers, sessions, tasks...) extends `Cline::Sc
877
877
  - **Declarative attributes** — `attribute :my_field, :string` mirrors the JSON schema.
878
878
  - **Automatic camelCase↔snake_case mapping** — Cline stores `autoUpdateEnabled`, Ruby gets `auto_update_enabled`.
879
879
  - **Extra attribute preservation** — unknown JSON keys are stored in `extra_attributes` and re-serialized back, maintaining round-trip fidelity.
880
+ - **Typed containers & unions** — `Utils::Schema.collection(Type)` models arrays, `Utils::Schema.map(:string)` dynamic-key maps and `Utils::Schema.union(:string, Utils::Schema.collection(Type))` attributes that can be of several types.
880
881
 
881
882
  3 mixin modules handle file persistence — classes include them to "come alive" from disk:
882
883
 
@@ -0,0 +1,116 @@
1
+ module Cline
2
+ # Base class for schemas whose data may not respect the expected types on deserialization.
3
+ # This is typically the case for attributes filled by an LLM, whose types are not guaranteed
4
+ # by the Cline CLI (for example a String given where an Array is expected).
5
+ #
6
+ # Contrary to {Schema}, deserialization does not raise exceptions for such wrong data:
7
+ # a warning is emitted, every attribute that can be deserialized properly is set on the instance,
8
+ # and the other attributes are stored in {#wrong_attributes}, keyed by their attribute name.
9
+ #
10
+ # @example
11
+ # input = Cline::SessionMessage::MessageContent::ToolUseInput.of_hash(
12
+ # 'question' => 'Continue?',
13
+ # 'files' => 'not-an-array'
14
+ # )
15
+ # input.question #=> 'Continue?'
16
+ # input.files #=> nil
17
+ # input.wrong_attributes #=> { files: 'not-an-array' }
18
+ class LenientSchema < Schema
19
+ extend Utils::Logger
20
+
21
+ # @!group Public API
22
+
23
+ # The attributes that could not be deserialized, keyed by their attribute name, with their raw value.
24
+ # Only set when at least one attribute could not be deserialized.
25
+ # When the whole value is not a Hash, it is stored under the '_raw' key.
26
+ #
27
+ # @return [Hash{Symbol => Object}, nil] The wrong attributes, or nil if deserialization succeeded
28
+ attr_accessor :wrong_attributes
29
+
30
+ class << self
31
+ # @!group Internal
32
+
33
+ # Cast an input value to this Schema object.
34
+ # Any value that cannot be cast is turned into an empty instance with {#wrong_attributes} set,
35
+ # instead of raising an exception.
36
+ #
37
+ # @param value [Schema, Hash, nil] The value that could be used to initialize a new instance of this attribute
38
+ # @return [Schema, nil] The corresponding instance, or nil if none
39
+ def cast(value)
40
+ super
41
+ rescue StandardError => e
42
+ # TODO: Make this a warn message when logger will be implemented properly
43
+ log_debug "!!! Cline: Unable to cast a #{value.class} into a #{name} (#{e.class}: #{e.message}). Raw value: #{value.to_s[0, 256]}"
44
+ instance = new
45
+ instance.wrong_attributes = { '_raw' => value.to_s }
46
+ instance
47
+ end
48
+
49
+ # Parse a Hash object and instantiate the proper instance from it.
50
+ # Instead of raising an exception when an attribute cannot be deserialized
51
+ # (wrong type, wrong structure...), this attribute is left unset and its raw value
52
+ # is stored in {#wrong_attributes}, while a warning is emitted.
53
+ # Attributes that can be deserialized properly are still set on the instance.
54
+ #
55
+ # @param hash [Hash] Data
56
+ # @param args [Array] Remaining arguments to be transferred to Shale
57
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
58
+ # @return [LenientSchema] Corresponding instance
59
+ def of_hash(hash, *args, **kwargs)
60
+ complete_hash_mapping
61
+ instance = new
62
+ unless hash.is_a?(Hash)
63
+ # TODO: Make this a warn message when logger will be implemented properly
64
+ log_debug "Cline: Unable to deserialize a #{name} from a #{hash.class}. Raw value: #{hash.to_s[0, 256]}"
65
+ instance.wrong_attributes = { '_raw' => hash.to_s }
66
+ return instance
67
+ end
68
+ mappings = hash_mapping.keys
69
+ wrong_attributes = {}
70
+ hash.each do |key, value|
71
+ mapping = mappings[key]
72
+ if mapping.nil?
73
+ # Keep unknown attributes for round-trip fidelity, as Schema does
74
+ (instance.extra_attributes ||= {})[key] = value
75
+ next
76
+ end
77
+ begin
78
+ set_attribute_from_hash(instance, mapping, value, *args, **kwargs)
79
+ rescue StandardError => e
80
+ # TODO: Make this a warn message when logger will be implemented properly
81
+ log_debug "Cline: Unable to deserialize attribute #{key.inspect} of #{name} (#{e.class}: #{e.message}). Raw value: #{value.to_s[0, 256]}"
82
+ wrong_attributes[mapping.attribute] = value
83
+ end
84
+ end
85
+ instance.wrong_attributes = wrong_attributes unless wrong_attributes.empty?
86
+ instance
87
+ end
88
+
89
+ private
90
+
91
+ # Set one attribute of an instance from its Hash value, mirroring Shale's own of_hash logic.
92
+ #
93
+ # @param instance [LenientSchema] The instance to set the attribute on
94
+ # @param mapping [Shale::Mapping::Descriptor::Dict] The hash mapping of the attribute
95
+ # @param value [Object] The raw value to deserialize
96
+ # @param args [Array] Remaining arguments to be transferred to Shale
97
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
98
+ # @return [void]
99
+ def set_attribute_from_hash(instance, mapping, value, *args, **kwargs)
100
+ attribute = attributes[mapping.attribute]
101
+ check_scalar_value!(attribute, value)
102
+ casted_value =
103
+ if value.nil?
104
+ nil
105
+ elsif attribute.collection?
106
+ (value.is_a?(Array) ? value : [value]).map do |element|
107
+ element.nil? ? nil : attribute.type.cast(attribute.type.of_hash(element, *args, **kwargs))
108
+ end
109
+ else
110
+ attribute.type.cast(attribute.type.of_hash(value, *args, **kwargs))
111
+ end
112
+ instance.send(attribute.setter, casted_value)
113
+ end
114
+ end
115
+ end
116
+ end
data/lib/cline/schema.rb CHANGED
@@ -10,6 +10,8 @@ module Cline
10
10
  class Schema < Shale::Mapper
11
11
  # @!group Internal
12
12
 
13
+ include Utils::Logger
14
+
13
15
  # @return [Hash] Store all extra values from a JSON parse
14
16
  attr_accessor :extra_attributes
15
17
 
@@ -23,6 +25,31 @@ module Cline
23
25
  @snake_attributes.uniq!
24
26
  end
25
27
 
28
+ # Enable strict type checks on deserialization.
29
+ # When enabled, a scalar attribute that receives an Array or a Hash raises a TypeError
30
+ # instead of being silently coerced.
31
+ #
32
+ # @return [void]
33
+ def strict_types!
34
+ @strict_types = true
35
+ end
36
+
37
+ # Are strict type checks enabled on deserialization?
38
+ #
39
+ # @return [Boolean] true if strict type checks are enabled
40
+ def strict_types?
41
+ @strict_types == true
42
+ end
43
+
44
+ # Check if a raw value can be deserialized by this schema.
45
+ # By default a schema is deserialized from a Hash, or from an instance of itself.
46
+ #
47
+ # @param raw_value [Object] The raw value to check
48
+ # @return [Boolean] true if {of_hash} can deserialize this value
49
+ def accepts_value?(raw_value)
50
+ raw_value.is_a?(self) || raw_value.is_a?(Hash)
51
+ end
52
+
26
53
  # Parse a Hash object and instantiate the proper instance from it.
27
54
  #
28
55
  # @param hash [Hash] Data
@@ -42,6 +69,7 @@ module Cline
42
69
  extra_hash[key] = value
43
70
  end
44
71
  end
72
+ validate_scalar_types!(known_hash) if strict_types?
45
73
  # Give Shale the data it knows about, without extra attributes
46
74
  instance = super(known_hash, *args, **kwargs)
47
75
  instance.extra_attributes = extra_hash unless extra_hash.empty?
@@ -81,6 +109,30 @@ module Cline
81
109
 
82
110
  private
83
111
 
112
+ # Validate that scalar attributes do not receive a structured value.
113
+ #
114
+ # @param known_hash [Hash] The known attributes to validate
115
+ # @return [void]
116
+ def validate_scalar_types!(known_hash)
117
+ known_hash.each do |key, value|
118
+ attribute = attributes[hash_mapping.keys[key].attribute]
119
+ check_scalar_value!(attribute, value) if attribute
120
+ end
121
+ end
122
+
123
+ # Check that a value is structurally compatible with an attribute.
124
+ # A scalar attribute should not receive an Array or a Hash.
125
+ #
126
+ # @param attribute [Shale::Attribute] The attribute to check
127
+ # @param value [Object] The raw value to check
128
+ # @return [void]
129
+ def check_scalar_value!(attribute, value)
130
+ return if value.nil? || attribute.collection? || attribute.type < Shale::Type::Complex
131
+ return unless value.is_a?(Array) || value.is_a?(Hash)
132
+
133
+ raise TypeError, "Attribute #{attribute.name.inspect} of #{name} expects a scalar value but got a #{value.class}"
134
+ end
135
+
84
136
  # Complete the hash mapping to include camelCase to snake_case as the defaults.
85
137
  # Do it only once.
86
138
  def complete_hash_mapping
@@ -46,13 +46,18 @@ module Cline
46
46
  # @!group Public API
47
47
 
48
48
  # An input used for tool use content.
49
- class ToolUseInput < Schema
49
+ # Its values are filled by the LLM, whose types are not guaranteed by the Cline CLI:
50
+ # deserialization is therefore lenient (wrong values do not raise but are stored in {#wrong_attributes}).
51
+ class ToolUseInput < LenientSchema
50
52
  # @!group Public API
51
53
 
52
54
  # A file to be read by the read_files tool
55
+ # The path is expected to be a String: a wrong type (e.g. an Array) is reported as a deserialization error.
53
56
  class FileInput < Schema
54
57
  # @!group Public API
55
58
 
59
+ strict_types!
60
+
56
61
  # @return [String] File path
57
62
  attribute :path, :string
58
63
  end
@@ -81,6 +86,27 @@ module Cline
81
86
  cline_snake_attributes :new_text, :old_text
82
87
  end
83
88
 
89
+ # A content entry of a tool result that is not a plain String.
90
+ class ToolResultContent < Schema
91
+ # @!group Public API
92
+
93
+ # @return [String, nil] Query or command executed by the tool
94
+ attribute :query, :string
95
+
96
+ # @return [String, nil] Result of the query or command
97
+ attribute :result, :string
98
+
99
+ # @return [Boolean, nil] Whether the query or command succeeded
100
+ attribute :success, :boolean
101
+
102
+ # Get a human-readable representation of this tool result.
103
+ #
104
+ # @return [String] The result of the tool, or its query if there is no result
105
+ def to_s
106
+ result || query || ''
107
+ end
108
+ end
109
+
84
110
  # @return [String] Content block type (text, tool_use, tool_result)
85
111
  attribute :type, :string
86
112
 
@@ -102,8 +128,8 @@ module Cline
102
128
  # @return [String, nil] Tool use identifier this result corresponds to (for type "tool_result")
103
129
  attribute :tool_use_id, :string
104
130
 
105
- # @return [String, nil] Content of the tool result (for type "tool_result")
106
- attribute :content, :string
131
+ # @return [String, Array<ToolResultContent>, nil] Content of the tool result (for type "tool_result")
132
+ attribute :content, Utils::Schema.union(:string, Utils::Schema.collection(ToolResultContent))
107
133
 
108
134
  cline_snake_attributes :tool_use_id
109
135
  end
@@ -169,7 +195,7 @@ module Cline
169
195
  "Assistant: #{first_text}"
170
196
  end
171
197
  when 'tool'
172
- "Tool result: #{content.find { |c| c.type == 'tool_result' }&.content}"
198
+ "Tool result: #{human_tool_result}"
173
199
  else
174
200
  to_s
175
201
  end
@@ -202,5 +228,15 @@ module Cline
202
228
  def first_text
203
229
  content.find { |c| c.type == 'text' }&.text.to_s.gsub("\n", ' ')
204
230
  end
231
+
232
+ # Get a human representation of the tool result content of this message.
233
+ #
234
+ # @return [String] The tool result content, as a String
235
+ def human_tool_result
236
+ tool_result = content.find { |c| c.type == 'tool_result' }&.content
237
+ return tool_result.to_s if tool_result.nil? || tool_result.is_a?(String)
238
+
239
+ Array(tool_result).join(' ')
240
+ end
205
241
  end
206
242
  end
@@ -74,6 +74,14 @@ module Cline
74
74
  subclass.value_type = value_type
75
75
  end
76
76
 
77
+ # Check if a raw value can be deserialized by this map.
78
+ #
79
+ # @param raw_value [Object] The raw value to check
80
+ # @return [Boolean] true if this map can deserialize this value
81
+ def accepts_value?(raw_value)
82
+ raw_value.is_a?(self) || raw_value.is_a?(Hash)
83
+ end
84
+
77
85
  # Parse a Hash object and instantiate the proper instance from it.
78
86
  #
79
87
  # @param hash [Hash] Data
@@ -233,6 +241,14 @@ module Cline
233
241
  subclass.value_type = value_type
234
242
  end
235
243
 
244
+ # Check if a raw value can be deserialized by this collection.
245
+ #
246
+ # @param raw_value [Object] The raw value to check
247
+ # @return [Boolean] true if this collection can deserialize this value
248
+ def accepts_value?(raw_value)
249
+ raw_value.is_a?(self) || raw_value.is_a?(Array)
250
+ end
251
+
236
252
  # Parse a Ruby object and instantiate the proper instance from it.
237
253
  #
238
254
  # @param hash [Object] Data
@@ -285,6 +301,145 @@ module Cline
285
301
  end
286
302
  Schema.const_get(external_name)
287
303
  end
304
+
305
+ # Provide a class that can be used by Shale for attributes that can be of several types, like this:
306
+ # "content": "Some text" # A String
307
+ # "content": [{ "query": "ls" }, ...] # An Array of structures
308
+ # The first type accepting a value is used to deserialize / serialize it.
309
+ #
310
+ # @param shale_types [Array<Symbol, Class>] The Shale types this attribute can be (at least 2)
311
+ # @return [Class] Class that can be used in an attribute declaration
312
+ def self.union(*shale_types)
313
+ Union.of(*shale_types)
314
+ end
315
+
316
+ # Handle union types: attributes that can be of several types.
317
+ # Used by {Cline::Utils::Schema.union}.
318
+ class Union
319
+ # @!group Internal
320
+
321
+ # Ruby classes that can be deserialized by each scalar Shale type.
322
+ # Shale types not listed here are considered to accept any value.
323
+ SCALAR_VALUE_CLASSES = {
324
+ Shale::Type::String => String,
325
+ Shale::Type::Integer => Integer,
326
+ Shale::Type::Float => Float,
327
+ Shale::Type::Boolean => [TrueClass, FalseClass],
328
+ Shale::Type::Date => Date,
329
+ Shale::Type::Time => Time,
330
+ Shale::Type::Decimal => BigDecimal
331
+ }.freeze
332
+
333
+ class << self
334
+ # Build (or retrieve) the type class for a union of the given types.
335
+ #
336
+ # @param shale_types [Array<Symbol, Class>] The Shale types this union can be (at least 2)
337
+ # @return [Class] Class that can be used in an attribute declaration
338
+ def of(*shale_types)
339
+ raise ArgumentError, "A union type needs at least 2 types, got #{shale_types.size}" if shale_types.size < 2
340
+
341
+ shale_types = shale_types.map { |shale_type| shale_type.is_a?(Symbol) ? Shale::Type.lookup(shale_type) : shale_type }
342
+ # Give a name to this class because Shale needs it to reference it in its accessors.
343
+ # That also allows us to not redefine the same class several times.
344
+ external_name = :"UnionOf#{shale_types.map { |shale_type| shale_type.to_s.gsub(':', '') }.join('Or')}"
345
+ unless Cline::Utils::Schema.const_defined?(external_name)
346
+ union_class = new_union_class
347
+ union_class.union_types = shale_types
348
+ union_class.external_name = external_name
349
+ Cline::Utils::Schema.const_set(external_name, union_class)
350
+ end
351
+ Cline::Utils::Schema.const_get(external_name)
352
+ end
353
+
354
+ # Check if a raw value can be deserialized by a given Shale type.
355
+ # Shale types that don't handle accepted values are considered as scalar types.
356
+ #
357
+ # @param shale_type [Class] The Shale type to check
358
+ # @param raw_value [Object] The raw value to check
359
+ # @return [Boolean] true if the Shale type can deserialize this value
360
+ def accepts_value?(shale_type, raw_value)
361
+ if shale_type.respond_to?(:accepts_value?)
362
+ shale_type.accepts_value?(raw_value)
363
+ else
364
+ raw_value.is_a?(SCALAR_VALUE_CLASSES.fetch(shale_type, Object))
365
+ end
366
+ end
367
+
368
+ private
369
+
370
+ # Create the class implementing a union type.
371
+ #
372
+ # @return [Class] The new union type class
373
+ def new_union_class
374
+ Class.new(Cline::Schema) do
375
+ class << self
376
+ attr_accessor :union_types, :external_name
377
+
378
+ # Check if a raw value can be deserialized by one of this union's types.
379
+ #
380
+ # @param raw_value [Object] The raw value to check
381
+ # @return [Boolean] true if one of this union's types accepts this value
382
+ def accepts_value?(raw_value)
383
+ raw_value.is_a?(self) ||
384
+ union_types.any? { |union_type| Cline::Utils::Schema::Union.accepts_value?(union_type, raw_value) }
385
+ end
386
+
387
+ # Parse a value and instantiate the proper instance from it.
388
+ #
389
+ # @param value [Object] Data
390
+ # @param args [Array] Remaining arguments to be transferred to Shale
391
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
392
+ # @return [Object, nil] Corresponding instance, or nil if value is nil
393
+ def of_hash(value, *args, **kwargs)
394
+ return nil if value.nil?
395
+
396
+ union_type_for(value).of_hash(value, *args, **kwargs)
397
+ end
398
+
399
+ # Get a Ruby object from an instance.
400
+ #
401
+ # @param value [Object] Object to serialize
402
+ # @param args [Array] Remaining arguments to be transferred to Shale
403
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
404
+ # @return [Object, nil] Corresponding Ruby object, or nil if value is nil
405
+ def as_hash(value, *args, **kwargs)
406
+ return nil if value.nil?
407
+
408
+ union_type_for(value).as_hash(value, *args, **kwargs)
409
+ end
410
+
411
+ # Cast an input value to the type matching it.
412
+ #
413
+ # @param value [Object, nil] The value to cast
414
+ # @return [Object, nil] The corresponding instance, or nil if none
415
+ def cast(value)
416
+ return nil if value.nil?
417
+
418
+ union_type_for(value).cast(value)
419
+ end
420
+
421
+ # Return the class name
422
+ #
423
+ # @return [String] The class name
424
+ def to_s
425
+ "::Cline::Utils::Schema::#{@external_name}"
426
+ end
427
+
428
+ private
429
+
430
+ # Get the type that can deserialize / serialize a given value.
431
+ #
432
+ # @param value [Object] The value to find the type of
433
+ # @return [Class] The corresponding type
434
+ def union_type_for(value)
435
+ union_types.find { |union_type| Cline::Utils::Schema::Union.accepts_value?(union_type, value) } ||
436
+ raise("Unable to cast a #{value.class} into #{name}")
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end
442
+ end
288
443
  end
289
444
  end
290
445
  end
data/lib/cline/version.rb CHANGED
@@ -2,5 +2,5 @@ module Cline
2
2
  # @!group Public API
3
3
 
4
4
  # Gem version
5
- VERSION = '2.2.0'
5
+ VERSION = '2.3.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cline-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
@@ -187,6 +187,7 @@ files:
187
187
  - lib/cline/global_state/models.rb
188
188
  - lib/cline/global_state/toggles.rb
189
189
  - lib/cline/global_state/workspace.rb
190
+ - lib/cline/lenient_schema.rb
190
191
  - lib/cline/log.rb
191
192
  - lib/cline/logs.rb
192
193
  - lib/cline/mcp_settings.rb