cline-rb 1.0.0 → 1.1.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: f2ca7f9e00479a00256f8b855bac70016da1a7a593a5912900820166f1e35d72
4
- data.tar.gz: 39c24569d60b34127173ae7aac99600b89d419de0443cbd2edbc26a9c7beb509
3
+ metadata.gz: 61431cfcf1fb2275838e3781f564c4697ae093a2605ebd8c2dd579ce10326939
4
+ data.tar.gz: 1604c7050d3d0e21a8d716207f4a43311891bf9205c6372b33df43a971070821
5
5
  SHA512:
6
- metadata.gz: f64d28459324a8251823c224d605c748c062932ff2dfa83006ee58b6c2b1c364fe8678f7ea5424422b0241c9d0fd375b215ebd19e929d35df7d1d95cd7047b30
7
- data.tar.gz: 9e93540a9bab1f9cf19adc560057b0b8d20bb095ee6e469d112efc4ddc740f7f88eb624ce6295853bf763497d138ea6d285ee3b621edff206eab0d1d5c355e40
6
+ metadata.gz: d675729aaf7d8dc1c6f88164899545cc9e321c1ffcb76f3065fa19f7bffcceeb9a87f7c20de9e1612e08f38431c0b66b82d76ad7b22ab0f0cd0f522de0036063
7
+ data.tar.gz: 23b351b69ccdd74b03e1ced53a7330f813e8eb682051056e4e7d110957f8bc1cca08b751a6047c6b3c4a4d5d97dccf408ab21d67ba35f256031756528cc426be
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # [v1.1.0](https://github.com/Muriel-Salvan/cline-rb/compare/v1.0.0...v1.1.0) (2026-07-01 13:34:01)
2
+
3
+ ### Features
4
+
5
+ * [[feature] feat: include CliStub spec files in gem and document usage in README](https://github.com/Muriel-Salvan/cline-rb/commit/6d8103fc559194396cd50508728625c82e8a3f19)
6
+
1
7
  # [v0.0.1](https://github.com/Muriel-Salvan/cline-rb/compare/...v0.0.1) (2026-07-01 12:59:01)
2
8
 
3
9
  ### Patches
data/README.md CHANGED
@@ -103,6 +103,7 @@ Designed as a Ruby library (not a standalone CLI), **cline-rb** lets you build a
103
103
  - [Install dependencies](#install-dependencies)
104
104
  - [Project structure](#project-structure)
105
105
  - [Running tests](#running-tests)
106
+ - [Using the Cline CLI stub in other project's test cases](#using-the-cline-cli-stub-in-other-projects-test-cases)
106
107
  - [Code linting](#code-linting)
107
108
  - [Code coverage](#code-coverage)
108
109
  - [Building the gem](#building-the-gem)
@@ -1040,6 +1041,42 @@ TEST_DEBUG=1 bundle exec rspec
1040
1041
 
1041
1042
  The `.rspec` file configures `--color` and `--require spec_helper` by default.
1042
1043
 
1044
+ ### Using the Cline CLI stub in other project's test cases
1045
+
1046
+ The `cline-rb` gem ships with a `CliStub` class that external projects can use to mock the Cline CLI in their own unit tests. It intercepts `PTY.spawn` calls and replaces them with a lightweight stub that simulates Cline CLI behaviour (stdout, sessions, tasks, logs, exit codes, etc.).
1047
+
1048
+ Example usage:
1049
+
1050
+ ```ruby
1051
+ require "#{Gem.loaded_specs['cline-rb'].full_gem_path}/spec/cline_test/cli_stub"
1052
+
1053
+ it 'tests something in my project' do
1054
+ # Setup the Cline CLI stub
1055
+ cli_stub = ClineTest::CliStub.new
1056
+ cli_stub.mock_commands(
1057
+ {
1058
+ log: {},
1059
+ session: {
1060
+ messages: [
1061
+ {
1062
+ ts: 100,
1063
+ role: 'assistant',
1064
+ content: [{ type: 'text', text: 'Assistant Output' }]
1065
+ }
1066
+ ]
1067
+ }
1068
+ }
1069
+ )
1070
+ # Run the code that would have executed the Cline CLI
1071
+ result = Cline::Cli.new(config: '.test-config').task 'A nice user prompt'
1072
+ expect(result[:message].content.first.text).to eq 'Assistant Output'
1073
+ # Use the stub to check what was called
1074
+ expect(cli_stub.issued_commands.first[:command]).to eq ['--config', '.test-config', 'A nice user prompt']
1075
+ end
1076
+ ```
1077
+
1078
+ For full API documentation of the `CliStub` class, see the [ClineTest::CliStub YARD documentation](https://www.rubydoc.info/gems/cline-rb/ClineTest/CliStub).
1079
+
1043
1080
  ### Code linting
1044
1081
 
1045
1082
  The project uses **RuboCop** (~> 1.86) with `rubocop-rspec` and `rubocop-yard` plugins.
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 = '1.0.0'
5
+ VERSION = '1.1.0'
6
6
  end
@@ -0,0 +1,209 @@
1
+ require 'pty_compat'
2
+
3
+ module ClineTest
4
+ # Stub of the Cline CLI (mocking calls to PTY.spawn).
5
+ # This can be used by external projects that need to mock or stub Cline CLI in their own unit tests.
6
+ class CliStub
7
+ class << self
8
+ # Capture the original PTY.spawn method in case some test cases want to use it while the real one is mocked.
9
+ attr_accessor :original_pty_spawn
10
+
11
+ # @return [Boolean] The debug mode.
12
+ attr_accessor :debug
13
+
14
+ # Log debug a message
15
+ #
16
+ # @param message [String, nil] The message to log debug, or nil if given by a proc returning the message for lazy evaluation
17
+ # @yield The optional code returning the message to log in case of debug
18
+ # @yieldreturn [String] The message to log
19
+ def log_debug(message = nil)
20
+ return unless @debug
21
+
22
+ puts "[CLINE STUB DEBUG] - #{block_given? ? yield : message}"
23
+ end
24
+ end
25
+ self.original_pty_spawn = ::PTY.method(:spawn)
26
+
27
+ # Constructor
28
+ #
29
+ # @param example [Object] The RSpec example for which the stub is executed
30
+ # @param debug [Boolean] Are we in debug mode?
31
+ # @param temp_dir [String] Temporary directory used to communicate with the stub
32
+ def initialize(example:, debug: false, temp_dir: '.cline_test/tmp')
33
+ @example = example
34
+ CliStub.debug = debug
35
+ @temp_dir = temp_dir
36
+ end
37
+
38
+ # Mock a list of commands, with their corresponding stdout, stderr and exit status.
39
+ # This helper hides the underlying ways of running commands from Cline::Cli.
40
+ # It uses a Cline CLI stub that executes mocked commands in place of the real Cline CLI.
41
+ #
42
+ # @param commands [Hash, Array] The description of the mocked behaviour the Cline CLI stub will have.
43
+ # This parameter can be of 2 kinds:
44
+ # - [Hash{Array<String, Regexp> => Hash{Symbol => Object}, Array<Hash{Symbol => Object}>}] Describe a list of (or a single) instructions,
45
+ # for each command to mock.
46
+ # The command to be mocked is the array of Cline arguments (after the cline executable).
47
+ # Each argument from this command line array can be either a String for exact match or a Regexp for pattern matching.
48
+ # Each mocked instruction is described below.
49
+ # - [Array<Hash{Symbol => Object}, Array<Hash{Symbol => Object}>>] Describe a sequential list of groups (or single) instructions.
50
+ # Each group of instruction will be executed for each new command that gets executed, regardless of its arguments.
51
+ # Each mocked instruction is described below.
52
+ #
53
+ # Each instruction is a [Hash{Symbol => Object}] that describes the behaviour to mock.
54
+ # They are executed in sequence of the list they belong to, and in sequence of the keys inside each Hash.
55
+ # Here is the possible instructions that are available:
56
+ # - debug [Boolean] Set debug mode.
57
+ # - eval [String] Execute some code.
58
+ # - exit [Integer] Exit with the given exit status.
59
+ # - log [Hash, String] Add a line in the Cline logs, either as a JSON Hash or a raw String.
60
+ # This property should be used only with a command using the --config flag.
61
+ # - session [Hash{Symbol => Object}] Create a session.
62
+ # This property should be used only with a command using the --config flag.
63
+ # Here is the list of all properties that can be set for the session description:
64
+ # - Any attribute that is in a session JSON file.
65
+ # - messages [Array<Hash{Symbol => Object}, Array<Hash{Symbol => Object}>>, nil] List of messages (or messages groups) to be created,
66
+ # or nil if none.
67
+ # Each message (or group) from the list will be created with 0.2 seconds interval.
68
+ # If the message's text content is a Hash with an eval key, the text content is replaced by the corresponding code execution.
69
+ # - sleep [Float] Sleep for a given time in seconds.
70
+ # - stderr [String] Output a string to stderr.
71
+ # - stdout [String] Output a string to stdout.
72
+ # - task [Hash{Symbol => Object}] Create a task.
73
+ # This property should be used only with a command using the --config flag.
74
+ # Here is the list of all properties that can be set for the task description:
75
+ # - messages [Array<Hash{Symbol => Object}, Array<Hash{Symbol => Object}>>, nil] List of messages (or messages groups) to be created,
76
+ # or nil if none.
77
+ # Each message (or group) from the list will be created with 0.2 seconds interval.
78
+ def mock_commands(commands = {})
79
+ temp_dir = @temp_dir
80
+ run_idx = 0
81
+ @example.instance_eval do
82
+ # Mock `PTY.spawn(*args) do |reader, writer, pid|` with spies pattern
83
+ allow(::PTY).to receive(:spawn) do |*args, &block|
84
+ cline_args = args[(args.find_index { |arg| arg.end_with?('cline') } + 1)..]
85
+ # Find the instructions corresponding to this Cline CLI run we want to mock
86
+ instructions =
87
+ if commands.is_a?(Hash)
88
+ # Ignore the verbose flag for command research if we activated the debug mode on purpose
89
+ cline_args_search = CliStub.debug ? cline_args - ['--verbose'] : cline_args
90
+ _mocked_command, found_instructions = commands.find do |search_command, _search_instructions|
91
+ search_command.size == cline_args_search.size &&
92
+ search_command.zip(cline_args_search).all? { |search_arg, arg| search_arg.is_a?(Regexp) ? arg =~ search_arg : arg == search_arg }
93
+ end
94
+ found_instructions
95
+ else
96
+ commands[run_idx]
97
+ end
98
+ instructions ||= []
99
+ # Create the JSON file that will give all instructions to execute to our Cline CLI stub.
100
+ stub_conf_file = "#{temp_dir}/cli_stub.json"
101
+ FileUtils.mkdir_p File.dirname(stub_conf_file)
102
+ File.write(
103
+ stub_conf_file,
104
+ JSON.dump(
105
+ # Normalize instructions (always using Arrays, setting default values).
106
+ (
107
+ (@debug ? [{ debug: true }] : []) +
108
+ (instructions.is_a?(Array) ? instructions : [instructions])
109
+ ).map do |instructions_set|
110
+ normalized_instructions = instructions_set.dup
111
+ if normalized_instructions[:log].is_a?(Hash)
112
+ normalized_instructions[:log] = {
113
+ level: 30,
114
+ hostname: 'LOCALHOST',
115
+ name: 'cline.cli',
116
+ component: 'main',
117
+ properties: {
118
+ ulid: 'test-session-id'
119
+ }
120
+ }.merge(normalized_instructions[:log])
121
+ end
122
+ if normalized_instructions[:session]
123
+ normalized_instructions[:session] = {
124
+ version: 1,
125
+ session_id: 'test-session-id',
126
+ source: 'cli',
127
+ status: 'running',
128
+ interactive: false,
129
+ provider: 'cline',
130
+ model: 'deepseek/deepseek-v4-flash',
131
+ cwd: Dir.pwd,
132
+ workspace_root: Dir.pwd,
133
+ team_name: 'team-sjHpe',
134
+ enable_tools: true,
135
+ enable_spawn: true,
136
+ enable_teams: true
137
+ }.merge(normalized_instructions[:session])
138
+ if normalized_instructions[:session][:messages]
139
+ default_message = {
140
+ id: 'msg_id_1',
141
+ role: 'assistant',
142
+ content: [
143
+ {
144
+ type: 'text',
145
+ text: 'Message content'
146
+ }
147
+ ],
148
+ ts: 100
149
+ }
150
+ normalized_instructions[:session][:messages] = normalized_instructions[:session][:messages].map do |messages_group|
151
+ (messages_group.is_a?(Array) ? messages_group : [messages_group]).map { |message| default_message.merge(message) }
152
+ end
153
+ end
154
+ end
155
+ if normalized_instructions.dig(:task, :messages)
156
+ default_message = { ts: 100, type: 'say', say: 'text', text: 'Message content' }
157
+ normalized_instructions[:task][:messages] = normalized_instructions[:task][:messages].map do |messages_group|
158
+ (messages_group.is_a?(Array) ? messages_group : [messages_group]).map { |message| default_message.merge(message) }
159
+ end
160
+ end
161
+ normalized_instructions
162
+ end
163
+ )
164
+ )
165
+ # Run PTY.spawn with our stub instead of the real Cline CLI.
166
+ stubbed_cmd = ["ruby#{'.exe' if OS.windows?}", File.expand_path("#{__dir__}/stubs/cline")] + cline_args
167
+ CliStub.log_debug { "Execute `#{stubbed_cmd}` with stub conf:\n#{JSON.pretty_generate(JSON.parse(File.read(stub_conf_file)))}" }
168
+ # In Windows' Ruby implementation (ruby.exe) there is actually a bug that splits multiline arguments into separate arguments.
169
+ # This bug does not exist on Linux implementations.
170
+ # Because of that, we manually replace the new lines with a magic key so that the behaviour stays consistent with the real arguments
171
+ # that would have been sent to Cline CLI.
172
+ # Our stub is then doing the opposite conversion on Windows only.
173
+ stubbed_cmd.map! { |arg| arg.gsub("\n", '__CLINE_STUB__NEW_LINE__') } if OS.windows?
174
+ result = nil
175
+ original_cline_stub_dir = ENV.fetch('CLINE_STUB_DIR', nil)
176
+ ENV['CLINE_STUB_DIR'] = temp_dir
177
+ begin
178
+ result = CliStub.original_pty_spawn.call(*stubbed_cmd) do |reader, writer, pid|
179
+ if @debug
180
+ allow(reader).to receive(:each_line).and_wrap_original do |original_each_line, &each_line_block|
181
+ original_each_line.call do |line|
182
+ CliStub.log_debug "[Cline stub stdout] - #{Cline::Utils::Logger.sanitize_pty_output(line)}"
183
+ each_line_block.call(line)
184
+ end
185
+ end
186
+ end
187
+ block.call(reader, writer, pid)
188
+ end
189
+ ensure
190
+ ENV['CLINE_STUB_DIR'] = original_cline_stub_dir
191
+ end
192
+ run_idx += 1
193
+ result
194
+ end
195
+ end
196
+ end
197
+
198
+ # Get the list of Cline CLI commands that were issued during this test run
199
+ #
200
+ # @return [Array<Hash{Symbol => Object}>] List of commands that have been issued:
201
+ # * pid [Integer] The PID of the Cline process
202
+ # * command [Array<String>] The command itself
203
+ # * stdin [String, nil] The stdin that was redirected to this command, or nil if none
204
+ def issued_commands
205
+ calls_file = "#{@temp_dir}/cli_calls.json"
206
+ File.exist?(calls_file) ? JSON.parse(File.read(calls_file), symbolize_names: true) : []
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This is a stub of the cline command line.
4
+
5
+ # It looks for its stub configuration in "#{ENV['CLINE_STUB_DIR']}/cli_stub.json".
6
+ # The cli_stub.json file contains the attributes defined by the mock_commands method (see #ClineTest::Helpers::Cli#mock_commands).
7
+
8
+ # It also records all its call arguments and stdin in the file named "#{ENV['CLINE_STUB_DIR']}/cli_calls.json".
9
+ # Those records are appended to the file if it already exists.
10
+ # This file contains an Array of calls information:
11
+ # * pid [Integer] The PID of the process
12
+ # * command [String] Command line that was called
13
+ # * stdin [String, nil] The content of stdin, or nil if none
14
+
15
+ require 'fileutils'
16
+ require 'json'
17
+ require 'os'
18
+
19
+ cline_stub_dir = ENV.fetch('CLINE_STUB_DIR', nil)
20
+ raise 'CLINE_STUB_DIR environment variable should be set to the directory containing the Cline stub configuration' unless cline_stub_dir
21
+
22
+ # Ruby's Windows implementation splits multilines arguments.
23
+ # Therefore we manually replaced them with a magic key.
24
+ # Make sure they are converted back to real new lines, as it would be received by the real Cline CLI.
25
+ ARGV.map! { |arg| arg.gsub('__CLINE_STUB__NEW_LINE__', "\n") } if OS.windows?
26
+
27
+ # @return [String] The config directory
28
+ def config_dir
29
+ @config_dir ||= begin
30
+ config_match = ARGV.join(' ').match(/--config ([^\s]+)/)[1]
31
+ raise 'The Cline stub can only handle this instruction if used with the --config flag' unless config_match
32
+
33
+ config_match
34
+ end
35
+ end
36
+
37
+ # @return [Boolean] Are we in plan mode?
38
+ def plan_mode?
39
+ ARGV.include? '--plan'
40
+ end
41
+
42
+ # Return a session messages file content, for a given list of messages.
43
+ # Always prepend the user input as a user message.
44
+ #
45
+ # @param session_id [String] The session id
46
+ # @param messages [Array<Hash{Symbol => Object}>] List of messages
47
+ # @return [Hash] The JSON messages file content
48
+ # If the message's text content is Hash with an eval key, the text content is replaced by the corresponding code execution.
49
+ def session_messages(session_id, messages)
50
+ {
51
+ version: 1,
52
+ updated_at: Time.now.utc.strftime('%FT%T.%LZ'),
53
+ agent: 'lead',
54
+ sessionId: session_id,
55
+ messages: (
56
+ [
57
+ {
58
+ ts: 10,
59
+ role: 'user',
60
+ content: [{ type: 'text', text: "<user_input mode=\"#{plan_mode? ? 'plan' : 'act'}\">#{ARGV.last}</user_input>" }]
61
+ }
62
+ ] +
63
+ messages
64
+ ).map do |message|
65
+ if message[:content]
66
+ message.merge(
67
+ content: message[:content].map do |content|
68
+ if content[:text].is_a?(Hash) && content[:text][:eval]
69
+ content.merge(text: eval(content[:text][:eval]))
70
+ else
71
+ content
72
+ end
73
+ end
74
+ )
75
+ else
76
+ message
77
+ end
78
+ end,
79
+ system_prompt: 'You are Cline, an AI coding agent.'
80
+ }
81
+ end
82
+
83
+ # Write session messages for a given session ID
84
+ #
85
+ # @param session_id [String] The session ID
86
+ # @param messages [Array<Hash>] The messages to write
87
+ def write_session_messages(session_id, messages)
88
+ log_debug "[Session #{session_id}] - Write #{messages.size} messages"
89
+ File.write(
90
+ File.join(config_dir, 'data', 'sessions', session_id, "#{session_id}.messages.json"),
91
+ JSON.pretty_generate(session_messages(session_id, messages))
92
+ )
93
+ end
94
+
95
+ # Log debug a message
96
+ #
97
+ # @param message [String] Message to log debug
98
+ def log_debug(message)
99
+ puts "[CLINE STUB DEBUG] - #{message}" if @debug
100
+ end
101
+
102
+ # Make sure output is sent directly to the calling process
103
+ $stdout.sync = true
104
+ $stderr.sync = true
105
+
106
+ @debug = false
107
+
108
+ # Record the call
109
+ calls_file = "#{cline_stub_dir}/cli_calls.json"
110
+ calls = File.exist?(calls_file) ? JSON.parse(File.read(calls_file)) : []
111
+ File.write(
112
+ calls_file,
113
+ JSON.dump(
114
+ calls + [
115
+ {
116
+ pid: Process.pid,
117
+ command: ARGV,
118
+ stdin:
119
+ if $stdin.tty?
120
+ nil
121
+ else
122
+ stdin = $stdin.read
123
+ stdin.empty? ? nil : stdin
124
+ end
125
+ }
126
+ ]
127
+ )
128
+ )
129
+
130
+ # Read the config driven by mock_commands and execute all instructions in sequence
131
+ JSON.parse(File.read("#{cline_stub_dir}/cli_stub.json"), symbolize_names: true).each do |instructions|
132
+ instructions.each do |name, desc|
133
+ log_debug "Execute instruction #{name}..."
134
+ case name
135
+ when :debug
136
+ @debug = desc
137
+ when :eval
138
+ eval desc
139
+ when :exit
140
+ exit desc
141
+ when :log
142
+ log_file = File.join(config_dir, 'data', 'logs', 'cline.log')
143
+ FileUtils.mkdir_p(File.dirname(log_file))
144
+ File.write(
145
+ log_file,
146
+ "#{
147
+ if desc.is_a?(String)
148
+ desc
149
+ else
150
+ # Prepare default values that are process dependent
151
+ JSON.dump(
152
+ {
153
+ time: Time.now.utc.strftime('%FT%T.%LZ'),
154
+ pid: Process.pid
155
+ }.merge(desc)
156
+ )
157
+ end
158
+ }\n",
159
+ mode: 'a+'
160
+ )
161
+ when :session
162
+ session_messages = desc.delete(:messages)
163
+ session_dir = File.join(config_dir, 'data', 'sessions', desc[:session_id])
164
+ messages_file = File.join(session_dir, "#{desc[:session_id]}.messages.json")
165
+ FileUtils.mkdir_p(session_dir)
166
+ File.write(
167
+ File.join(session_dir, "#{desc[:session_id]}.json"),
168
+ JSON.pretty_generate(
169
+ {
170
+ # Prepare default values that are process dependent
171
+ pid: Process.pid,
172
+ started_at: Time.now.utc.strftime('%FT%T.%LZ'),
173
+ messages_path: messages_file
174
+ }.merge(desc)
175
+ )
176
+ )
177
+ if session_messages
178
+ # First create empty file
179
+ write_session_messages(desc[:session_id], [])
180
+ sleep 0.2
181
+ # Then add test messages 1 by 1 with 0.2 seconds delay
182
+ session_messages.size.times do |idx|
183
+ write_session_messages(desc[:session_id], session_messages[0..idx].flatten(1))
184
+ sleep 0.2
185
+ end
186
+ end
187
+ when :sleep
188
+ sleep desc
189
+ when :stderr
190
+ $stderr.write desc
191
+ when :stdout
192
+ $stdout.write desc
193
+ when :task
194
+ # Simulate a task being created
195
+ $stdout.write "{\"type\":\"task_started\",\"taskId\":\"12345\"}\n"
196
+ task_dir = File.join(config_dir, 'data', 'tasks', '12345')
197
+ FileUtils.mkdir_p(task_dir)
198
+ if desc[:messages]
199
+ messages_file = File.join(task_dir, 'ui_messages.json')
200
+ # First create empty file
201
+ File.write(messages_file, '[]')
202
+ sleep 0.2
203
+ # Then add test messages 1 by 1 with 0.2 seconds delay
204
+ desc[:messages].size.times do |idx|
205
+ File.write(messages_file, JSON.dump(desc[:messages][0..idx].flatten(1)))
206
+ sleep 0.2
207
+ end
208
+ end
209
+ end
210
+ end
211
+ 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: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
@@ -224,6 +224,8 @@ files:
224
224
  - lib/cline/workspace.rb
225
225
  - lib/cline/workspace_settings.rb
226
226
  - lib/cline/workspaces.rb
227
+ - spec/cline_test/cli_stub.rb
228
+ - spec/cline_test/stubs/cline
227
229
  homepage: https://github.com/Muriel-Salvan/cline-rb
228
230
  licenses:
229
231
  - BSD-3-Clause