cline-rb 1.0.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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +139 -0
  3. data/README.md +1216 -0
  4. data/TODO.md +2 -0
  5. data/lib/cline/cli.rb +373 -0
  6. data/lib/cline/config.rb +100 -0
  7. data/lib/cline/configuration.rb +23 -0
  8. data/lib/cline/data.rb +119 -0
  9. data/lib/cline/file_content.rb +33 -0
  10. data/lib/cline/global_settings.rb +17 -0
  11. data/lib/cline/global_state/api_providers.rb +48 -0
  12. data/lib/cline/global_state/auto_approval.rb +73 -0
  13. data/lib/cline/global_state/browser.rb +52 -0
  14. data/lib/cline/global_state/features.rb +56 -0
  15. data/lib/cline/global_state/general.rb +77 -0
  16. data/lib/cline/global_state/models.rb +127 -0
  17. data/lib/cline/global_state/toggles.rb +33 -0
  18. data/lib/cline/global_state/workspace.rb +41 -0
  19. data/lib/cline/global_state.rb +16 -0
  20. data/lib/cline/log.rb +288 -0
  21. data/lib/cline/logs.rb +136 -0
  22. data/lib/cline/mcp_settings.rb +30 -0
  23. data/lib/cline/model.rb +47 -0
  24. data/lib/cline/models.rb +11 -0
  25. data/lib/cline/overlay_hash.rb +125 -0
  26. data/lib/cline/providers.rb +59 -0
  27. data/lib/cline/schema.rb +144 -0
  28. data/lib/cline/secret_string.rb +83 -0
  29. data/lib/cline/secrets.rb +119 -0
  30. data/lib/cline/serializable/cline_data.rb +131 -0
  31. data/lib/cline/serializable/dir.rb +81 -0
  32. data/lib/cline/serializable/file.rb +106 -0
  33. data/lib/cline/session.rb +87 -0
  34. data/lib/cline/session_data.rb +154 -0
  35. data/lib/cline/session_message.rb +178 -0
  36. data/lib/cline/session_messages.rb +61 -0
  37. data/lib/cline/sessions.rb +30 -0
  38. data/lib/cline/skill.rb +148 -0
  39. data/lib/cline/skills.rb +8 -0
  40. data/lib/cline/task.rb +75 -0
  41. data/lib/cline/task_message.rb +247 -0
  42. data/lib/cline/task_messages.rb +11 -0
  43. data/lib/cline/tasks.rb +30 -0
  44. data/lib/cline/usage.rb +37 -0
  45. data/lib/cline/utils/enumerable_dir_objects.rb +103 -0
  46. data/lib/cline/utils/file.rb +71 -0
  47. data/lib/cline/utils/file_monitor.rb +56 -0
  48. data/lib/cline/utils/logger.rb +37 -0
  49. data/lib/cline/utils/os/linux.rb +43 -0
  50. data/lib/cline/utils/os/mingw32.rb +46 -0
  51. data/lib/cline/utils/os.rb +31 -0
  52. data/lib/cline/utils/schema.rb +290 -0
  53. data/lib/cline/version.rb +6 -0
  54. data/lib/cline/workspace.rb +25 -0
  55. data/lib/cline/workspace_settings.rb +29 -0
  56. data/lib/cline/workspaces.rb +8 -0
  57. data/lib/cline.rb +22 -0
  58. metadata +249 -0
data/TODO.md ADDED
@@ -0,0 +1,2 @@
1
+ * Create helpers for all global caches or ENV variables being protected in tests, and use them all in the global around.
2
+ * Add examples of usage in an examples directory.
data/lib/cline/cli.rb ADDED
@@ -0,0 +1,373 @@
1
+ require 'human_number'
2
+ require 'pty_compat'
3
+ require 'sys/proctable'
4
+
5
+ # Load the HumanNumber locale files, as it does not do it automatically.
6
+ # TODO: Remove this when human_number will be fixed.
7
+ I18n.load_path += Dir[File.join(File.join(Gem::Specification.find_by_name('human_number').gem_dir, 'lib'), 'locales', '*.yml')]
8
+
9
+ module Cline
10
+ # Provide a wrapper over the Cline CLI tool
11
+ class Cli
12
+ include Utils::Logger
13
+
14
+ # Unexpected exit status error
15
+ class UnexpectedExitStatusError < RuntimeError
16
+ end
17
+
18
+ # Unknown option
19
+ class UnknownOptionError < RuntimeError
20
+ end
21
+
22
+ # Unexpected interactive session
23
+ class UnexpectedInteractiveSessionError < RuntimeError
24
+ end
25
+
26
+ # Define all commands and their options
27
+ COMMANDS = {
28
+ # Global options
29
+ global: {
30
+ verbose: '--verbose',
31
+ cwd: '--cwd STRING',
32
+ config: '--config STRING'
33
+ },
34
+ auth: {
35
+ # Provider ID for quick setup (e.g., openai-native, anthropic, moonshot)
36
+ provider: '--provider STRING',
37
+ # API key for the provider
38
+ apikey: '--apikey STRING',
39
+ # Model ID to configure (e.g., gpt-4o, claude-sonnet-4-6, kimi-k2.5)
40
+ modelid: '--modelid STRING',
41
+ # Base URL (optional, only for openai provider)
42
+ baseurl: '--baseurl STRING'
43
+ },
44
+ task: {
45
+ # Run in plan mode
46
+ plan: '--plan',
47
+ # Output messages as JSON instead of styled text
48
+ json: '--json',
49
+ # Enable auto-approve all actions
50
+ auto_approve: '--auto-approve',
51
+ # Reasoning effort level between none|low|medium|high|xhigh
52
+ thinking: '--thinking STRING',
53
+ # Context compaction mode: agentic|basic|off
54
+ compaction: '--compaction STRIG',
55
+ # Open the terminal user interface (TUI) for interactive sessions
56
+ tui: '--tui',
57
+ # Session ID to resume, or nil for a new session
58
+ id: '--id STRING',
59
+ # Provider to use for the session
60
+ provider: '--provider STRING',
61
+ # API key to use for the session
62
+ key: '--key STRING',
63
+ # Model to use for the task
64
+ model: '--model STRING',
65
+ # Override the default system prompt
66
+ system: '--system STRING',
67
+ # Start a session that runs in the background hub
68
+ zen: '--zen',
69
+ # Number of maximum consecutive mistakes (retries) before exiting
70
+ retries: '--retries INTEGER',
71
+ # Optional timeout in seconds (applies only when provided)
72
+ timeout: '--timeout INTEGER',
73
+ # Run in Agent Client Protocol (ACP) mode for editor integration
74
+ acp: '--acp',
75
+ # Use isolated local state at this directory path
76
+ data_dir: '--data-dir STRING',
77
+ # Path to additional hooks directory for runtime hook injection
78
+ hooks_dir: '--hooks-dir STRING',
79
+ # Auto-create a detached git worktree under ~/.cline/worktrees/ and run the task there
80
+ worktree: '--worktree',
81
+ # Run the kanban app
82
+ kanban: '--kanban'
83
+ }
84
+ }
85
+
86
+ # @!group Public API
87
+
88
+ # Constructor
89
+ #
90
+ # @param stdout_echo [Boolean] Do we echo stdout of Cline CLI?
91
+ # @param kwargs [Hash{Symbol => Object}] Global options (see COMMANDS[:global])
92
+ def initialize(stdout_echo: false, **kwargs)
93
+ @global_options = parse_global_options(**kwargs)
94
+ @stdout_echo = stdout_echo
95
+ @config_dir = kwargs[:config]
96
+ @cline_pid = nil
97
+ # [Session] Session associated to this CLI run.
98
+ # The session is discovered using:
99
+ # 1. Cline logs appearing after executing CLI that contain a session ID.
100
+ # 2. The session corresponding to this ID.
101
+ @session = nil
102
+ end
103
+
104
+ # Authenticate the CLI
105
+ #
106
+ # @param kwargs [Hash{Symbol => Object}] Command options (see COMMANDS)
107
+ # @return [Hash{Symbol => Object}] A set of return properties (see #run_cli)
108
+ def auth(**kwargs)
109
+ run_cli(command: 'auth', args: parse_auth_options(**kwargs))
110
+ end
111
+
112
+ # Start a task by sending a prompt
113
+ #
114
+ # @param prompt [String, nil] The prompt, or nil if none
115
+ # @param on_message [#call, nil] Callback called for each new or updated message for the session of this prompt,
116
+ # or nil if none (see Session#monitor_message)
117
+ # @param on_question [#call, nil] Callback called for each question that is asked by the assistant, or nil if none.
118
+ # This should be set if an interactive session is expected.
119
+ # If a question is asked without a callback to handle it, an UnexpectedInteractiveSessionError exceptioon will be raised.
120
+ # - Param question [SessionMessage::MessageContent::ToolUseInput] Question input with possible options
121
+ # (see SessionMessage::MessageContent::ToolUseInput).
122
+ # - Return [String] The answer that the user should provide to this assistant's question.
123
+ # @param monitoring_interval_secs [Float] The monitoring interval in seconds
124
+ # @param kwargs [Hash{Symbol => Object}] Command options (see COMMANDS)
125
+ # @return [Hash{Symbol => Object}] A set of return properties (see #run_cli). Additionnally the following ones:
126
+ # - message [SessionMessage, nil] The last message of the session, or nil if none
127
+ # - status [String] The task status
128
+ # - error [Log, nil] In case of status "failed", get the last error log entry, or nil if no error.
129
+ def task(prompt, on_message: nil, on_question: nil, monitoring_interval_secs: 1, **kwargs)
130
+ result = {}
131
+ start_time = Time.now
132
+ stripped_prompt = prompt&.strip
133
+ task_args = parse_task_options(**kwargs)
134
+ # Use a prompt temporary file if the prompt is too long to fit in the command line.
135
+ (
136
+ if stripped_prompt && stripped_prompt.size + 2 > Utils::Os.max_cmd_length - (Utils::Os.cline_exe + task_args).map { |arg| "\"#{arg}\"" }.join(' ').size
137
+ proc do |&run_block|
138
+ Utils::File.with_temp_dir do |tmp_dir|
139
+ prompt_file = "#{tmp_dir}/prompt.txt"
140
+ File.write(prompt_file, stripped_prompt)
141
+ run_block.call(
142
+ "The user prompt is given to you in the file `#{File.expand_path(prompt_file)}`. " \
143
+ 'You have to read it and treat its content as the user prompt.'
144
+ )
145
+ end
146
+ end
147
+ else
148
+ proc { |&run_block| run_block.call(stripped_prompt) }
149
+ end
150
+ ).call do |prompt_arg|
151
+ session_monitor_thread = nil
152
+ cli_running = true
153
+ begin
154
+ result = run_cli(
155
+ args: task_args + (prompt_arg ? [prompt_arg] : []),
156
+ on_start: proc do |_reader, writer, _pid|
157
+ session_monitor_thread = Thread.new do
158
+ # Start monitoring logs to get the session ID.
159
+ # Wait for config and logs to exist.
160
+ sleep 0.1 while cli_running && !config&.logs
161
+ # Monitor logs to get the session ID
162
+ session_id = nil
163
+ config&.logs&.monitor(
164
+ on_log: proc do |log, _last|
165
+ session_id = log.properties.ulid if !session_id && log.is_a?(Log) && log.properties&.ulid
166
+ end,
167
+ from: start_time
168
+ ) do
169
+ sleep 0.1 while cli_running && !session_id
170
+ end
171
+ # If CLI has finished, session_id should be already discovered, unless the session could not be created.
172
+ # If CLI has not finished yet, the we have the session ID discovered already.
173
+ # So it means that if session_id is nil, there has been a problem (like core dump).
174
+ if session_id
175
+ log_debug "Found Cline session ID #{session_id}"
176
+ # First get Cline models
177
+ cline_models = config.data&.cline_models || Data.vscode&.cline_models
178
+ # Wait for the CLI to create the session for real
179
+ sleep 0.1 while cli_running && !config.sessions(cline_models:)
180
+ config.sessions(cline_models:) unless cli_running
181
+ while cli_running && !config.sessions[session_id]
182
+ sleep 0.1
183
+ config.sessions.refresh!
184
+ end
185
+ # If CLI has finished, then the session should exist, unless there has been a problem (like file system issue).
186
+ @session = config.sessions && config.sessions[session_id]
187
+ # Now monitor the session messages for reporting and possible user callback
188
+ # [Hash{Integer => Usage}] All usages, per timestamp, for logging purposes
189
+ usages = {}
190
+ @session&.monitor_messages(
191
+ on_message: proc do |message, last, previous_version|
192
+ log_debug do
193
+ usages[message.ts] = message.usage if message.usage
194
+ last_usage = usages.values.last
195
+ prefix = "[#{message.timestamp.strftime('%H:%M:%S')}]#{
196
+ unless last_usage.nil?
197
+ " (#{HumanNumber.currency(usages.values.map { |usage| usage.cost || 0.0 }.sum, currency_code: 'USD')} " \
198
+ "#{HumanNumber.human_number(last_usage.context_tokens, max_digits: 2)}" \
199
+ "/#{HumanNumber.human_number(last_usage.context_tokens_limit || 0, max_digits: 2)})"
200
+ end
201
+ } - "
202
+ "#{prefix}#{message.to_human(limit: 128 - prefix.size)}"
203
+ end
204
+ # Call the user callback if any
205
+ on_message&.call(message, last, previous_version)
206
+ # If the message is the last one and the agent has asked a question, call the corresponding callback
207
+ if last
208
+ last_content = message.content&.last
209
+ if last_content&.type == 'tool_use' && last_content.name == 'ask_question'
210
+ unless on_question
211
+ raise UnexpectedInteractiveSessionError,
212
+ "Unexpected interactive session with assistant asking question #{last_content.input&.question}"
213
+ end
214
+
215
+ writer.puts(on_question.call(last_content.input))
216
+ end
217
+ end
218
+ end,
219
+ monitoring_interval_secs:
220
+ ) do
221
+ sleep 0.1 while cli_running
222
+ end
223
+ end
224
+ end
225
+ end
226
+ )
227
+ ensure
228
+ cli_running = false
229
+ session_monitor_thread&.join
230
+ end
231
+ end
232
+ if @session
233
+ result[:message] = @session.messages&.reverse_each&.find { |message| message.role == 'assistant' }
234
+ result[:status] = @session.status
235
+ result[:error] = config.logs.logs(from: start_time).reverse_each.find { |log| log.severity == 'error' } if @session.status == 'failed'
236
+ end
237
+ result
238
+ end
239
+
240
+ # @return [Integer, nil] The PID of the running Cline process, if any
241
+ attr_reader :cline_pid
242
+
243
+ # @return [Session, nil] The current or last session handled by the Cline process, if any
244
+ attr_reader :session
245
+
246
+ # Interrupt the running Cline command
247
+ def interrupt
248
+ if cline_pid
249
+ log_debug "Interrupt current command with PID #{cline_pid}"
250
+ all_pids = [cline_pid] + get_child_pids_recursive(cline_pid)
251
+ log_debug "Found process tree PIDs: #{all_pids.join(', ')}"
252
+ @interrupted_on_purpose = true
253
+ all_pids.reverse.each do |pid|
254
+ log_debug "Kill process #{pid}"
255
+ Utils::Os.kill(pid)
256
+ end
257
+ else
258
+ log_debug 'No Cline command started, so no need to interrupt anything'
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ # Generate all methods that can parse kwargs to generate CLI options, for each known command.
265
+ # Those methods are named parse_#{command}_options.
266
+ COMMANDS.each do |command, options|
267
+ # Parse the options for a given command
268
+ #
269
+ # @param kwargs [Hash{Symbol => Object}] The options associated to the command
270
+ # @return [Array<String>] The corresponding CLI arguments
271
+ define_method(:"parse_#{command}_options") do |**kwargs|
272
+ kwargs.map do |option, value|
273
+ raise UnknownOptionError, "Unknown #{command} option #{option}" unless options.key?(option)
274
+
275
+ if value
276
+ cli_option, cli_arg = options[option].split
277
+ [cli_option] + [cli_arg.nil? ? nil : value.to_s]
278
+ end
279
+ end.flatten(1).compact
280
+ end
281
+ end
282
+
283
+ # Run a command on the Cline CLI
284
+ #
285
+ # @param command [Symbol, nil] The command to run, or nil if none
286
+ # @param args [Array<String>] Command arguments
287
+ # @param stdin_data [String, nil] Data to send to the standard input of the CLI, or nil if none
288
+ # @param expected_exit_status [Integer, nil] Expected exit status, or nil for no expectation
289
+ # @param on_start [#call, nil] Optional callback that is called when the process is started
290
+ # - Param reader [IO] The process reader descriptor (stdout and stderr).
291
+ # - Param writer [IO] The process writer descriptor (stdin).
292
+ # - Param pid [Integer] The process PID.
293
+ # @param on_stdout [#call, nil] Optional callback that is called for every line output on stdout
294
+ # - Param line [String] The stdout line (including potential \n)
295
+ # @return [Hash{Symbol => Object}] A set of return properties:
296
+ # - stdout [String] Full stdout
297
+ # - exit_status [Integer] Exit status
298
+ def run_cli(command: nil, args: [], stdin_data: nil, expected_exit_status: 0, on_start: nil, on_stdout: nil)
299
+ cmd = Utils::Os.cline_exe +
300
+ (command ? [command] : []) +
301
+ @global_options +
302
+ args
303
+ log_debug "Launch CLI `#{cmd}`..."
304
+ @interrupted_on_purpose = false
305
+ stdout_lines = []
306
+ exit_status = nil
307
+ PTY.spawn(*cmd) do |reader, writer, pid|
308
+ @cline_pid = pid
309
+ log_debug "Cline master process started with PID #{cline_pid}"
310
+ writer.write(stdin_data) if stdin_data
311
+ on_start&.call(reader, writer, pid)
312
+ begin
313
+ reader.each_line do |line|
314
+ stdout_lines << line
315
+ $stdout.write(Utils::Logger.sanitize_pty_output(line, colored: !Cline.config.debug)) if @stdout_echo
316
+ on_stdout&.call(line)
317
+ end
318
+ rescue Errno::EIO => e
319
+ # Child process finished
320
+ log_debug "Cline master process (PID #{cline_pid}) got terminated: #{e.message}"
321
+ end
322
+ exit_status = Process::Status.wait.exitstatus
323
+ log_debug do
324
+ "Cline master process (PID #{cline_pid}) exited with status: #{exit_status}#{' (interrupted on purpose)' if @interrupted_on_purpose}"
325
+ end
326
+ @cline_pid = nil
327
+ unless @stdout_echo
328
+ log_debug do
329
+ <<~EO_DEBUG
330
+ ===== Cline CLI output BEGIN...
331
+ #{Utils::Logger.sanitize_pty_output(stdout_lines.join)}
332
+ ===== Cline CLI output ...END
333
+ EO_DEBUG
334
+ end
335
+ end
336
+ if !@interrupted_on_purpose && !expected_exit_status.nil? && exit_status != expected_exit_status
337
+ raise UnexpectedExitStatusError, "Cline master process `#{cmd}` exited with status #{exit_status} (expected #{expected_exit_status})"
338
+ end
339
+ end
340
+
341
+ {
342
+ stdout: Utils::Logger.sanitize_pty_output(stdout_lines.join),
343
+ exit_status:
344
+ }
345
+ end
346
+
347
+ # Return the config object associated to this instance
348
+ #
349
+ # @return [Config] The config instance used by this Cli instance
350
+ def config
351
+ @config ||= @config_dir.nil? ? Config.global : Config.open(@config_dir)
352
+ end
353
+
354
+ # Get all child PIDs recursively for a given parent PID
355
+ #
356
+ # @param parent_pid [Integer] Parent process ID
357
+ # @return [Array<Integer>] All child and grandchild PIDs recursively
358
+ def get_child_pids_recursive(parent_pid)
359
+ child_pids = []
360
+ begin
361
+ Sys::ProcTable.ps.each do |process|
362
+ next unless process.ppid == parent_pid
363
+
364
+ child_pids << process.pid
365
+ child_pids.concat(get_child_pids_recursive(process.pid))
366
+ end
367
+ rescue StandardError
368
+ # Gracefully handle errors if processes disappear while enumerating
369
+ end
370
+ child_pids
371
+ end
372
+ end
373
+ end
@@ -0,0 +1,100 @@
1
+ require 'forwardable'
2
+
3
+ module Cline
4
+ # Accesses all configuration of a Cline directory.
5
+ # Wraps for example the content of ~/.cline.
6
+ # The following properties can be used while opening a Config directory:
7
+ # - include_project_config [Boolean] Do we include the project-specific objects as well in this
8
+ # configuration? Defaults to `true`.
9
+ class Config
10
+ extend Forwardable
11
+
12
+ # @!group Public API
13
+
14
+ # Get the main Cline config.
15
+ # The main Cline config is the global config, enriched with also project-specific objects (skills...)
16
+ #
17
+ # @return [Config] The main config for the current user
18
+ def self.main
19
+ @main ||= Config.open("#{Utils::Os.user_home_dir}/.cline", include_project_config: true)
20
+ end
21
+
22
+ # Get the global Cline config
23
+ #
24
+ # @return [Config] The global config for the current user
25
+ def self.global
26
+ @global ||= Config.open("#{Utils::Os.user_home_dir}/.cline", include_project_config: false)
27
+ end
28
+
29
+ # Get the project Cline config
30
+ #
31
+ # @return [Config] The project config for the current repository
32
+ def self.project
33
+ @project ||= Config.open('.cline')
34
+ end
35
+
36
+ include Serializable::Dir
37
+
38
+ # Give access to the data getters
39
+ def_delegators :data, *%i[global_settings global_state logs mcp_settings providers sessions tasks workspaces]
40
+
41
+ # Get skills from this config
42
+ #
43
+ # @param create [Boolean] Should the data be created if it does not exist?
44
+ # @return [OverlayHash, nil] Set of skills, including global ones and project ones if needed, or nil if non existent (see Skills).
45
+ def skills(create: self.create)
46
+ @skills ||= begin
47
+ skills_layers = ([Skills.open(subpath('skills'), create:)] + [project_config&.skills]).compact
48
+ skills_layers.empty? ? nil : OverlayHash.new(*skills_layers)
49
+ end
50
+ end
51
+
52
+ # Get the data directory from this config
53
+ #
54
+ # @param create [Boolean] Should the data be created if it does not exist?
55
+ # @return [Data] The Cline data directory content
56
+ def data(create: self.create)
57
+ @data ||= Data.open(subpath('data'), create:)
58
+ end
59
+
60
+ # Equality check
61
+ #
62
+ # @param other [Object] The other to check equality with
63
+ # @return [Boolean] True if objects are equal
64
+ def ==(other)
65
+ other.is_a?(Config) &&
66
+ other.skills == skills &&
67
+ other.data == data
68
+ end
69
+
70
+ # Return a Cli instance that uses this config
71
+ #
72
+ # @param kwargs [Hash{Symbol => Object}] Global options (see #Cli.COMMANDS[:global])
73
+ # @return [Cli] Cli instance that is running using this config
74
+ def cli(**kwargs)
75
+ Cli.new(config: dir, **kwargs)
76
+ end
77
+
78
+ # Refresh caches to reload data from disk.
79
+ def refresh!
80
+ @skills = nil
81
+ @data = nil
82
+ end
83
+
84
+ # @!group Internal
85
+
86
+ # Constructor
87
+ #
88
+ # @param include_project_config [Boolean] Do we include the project configuration in the objects read?
89
+ def initialize(include_project_config: true)
90
+ @include_project_config = include_project_config
91
+ end
92
+
93
+ private
94
+
95
+ # @return [Config, nil] The additional project config if needed, or nil if none.
96
+ def project_config
97
+ @include_project_config && dir != Config.project&.dir ? Config.project : nil
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,23 @@
1
+ module Cline
2
+ # Object storing a cline-rb Rubgem configuration
3
+ class Configuration
4
+ # @!group Public API
5
+
6
+ # @return [Boolean] Debug mode.
7
+ # Defaults to `true` if `CLINE_DEBUG` environment variable is set to 1.
8
+ attr_accessor :debug
9
+
10
+ # @return [String] Temporary directories root for debug. Defaults to `.cline-rb/tmp`
11
+ # This is used only if debug is `true`.
12
+ attr_accessor :temp_dir_root
13
+
14
+ # @!group Internal
15
+
16
+ # Constructor
17
+ def initialize
18
+ # Default values are set here
19
+ @debug = (ENV['CLINE_DEBUG'] == '1')
20
+ @temp_dir_root = '.cline-rb/tmp'
21
+ end
22
+ end
23
+ end
data/lib/cline/data.rb ADDED
@@ -0,0 +1,119 @@
1
+ require 'json'
2
+
3
+ module Cline
4
+ # Accesses the content of a Cline data directory.
5
+ # Wraps for example the content of ~/.cline/data
6
+ class Data
7
+ # @!group Public API
8
+
9
+ include Serializable::Dir
10
+
11
+ # Get the VSCode plugin Cline data dir
12
+ #
13
+ # @return [Data, nil] The data for the installed VSCode plugin, or nil if none
14
+ def self.vscode
15
+ @vscode ||= Data.open(
16
+ "#{
17
+ ENV['VSCODE_PORTABLE'] ? "#{ENV['VSCODE_PORTABLE']}/user-data" : "#{Utils::Os.user_app_data_dir}/Code"
18
+ }/User/globalStorage/saoudrizwan.claude-dev"
19
+ )
20
+ end
21
+
22
+ # Get the cached Cline models
23
+ #
24
+ # @param create [Boolean] Should the data be created if it does not exist?
25
+ # @return [Models] Cached Cline models
26
+ def cline_models(create: self.create)
27
+ @cline_models ||= Models.from_cline_data(dir, create:)
28
+ end
29
+
30
+ # Get global settings stored in this data directory
31
+ #
32
+ # @param create [Boolean] Should the data be created if it does not exist?
33
+ # @return [GlobalSettings, nil] Global settings stored in this data directory, or nil if none
34
+ def global_settings(create: self.create)
35
+ @global_settings ||= GlobalSettings.from_cline_data(dir, create:)
36
+ end
37
+
38
+ # Get global state stored in this data directory
39
+ #
40
+ # @param create [Boolean] Should the data be created if it does not exist?
41
+ # @return [GlobalState, nil] Global state stored in this data directory, or nil if none
42
+ def global_state(create: self.create)
43
+ @global_state ||= GlobalState.from_cline_data(dir, create:)
44
+ end
45
+
46
+ # Get the Cline logs
47
+ #
48
+ # @return [Logs] The Cline logs
49
+ def logs(create: self.create)
50
+ @logs ||= Logs.open(subpath('logs/cline.log'), default: create ? '' : nil)
51
+ end
52
+
53
+ # Get MCP settings stored in this data directory
54
+ #
55
+ # @param create [Boolean] Should the data be created if it does not exist?
56
+ # @return [McpSettings, nil] MCP settings stored in this data directory, or nil if none
57
+ def mcp_settings(create: self.create)
58
+ @mcp_settings ||= McpSettings.from_cline_data(dir, create:)
59
+ end
60
+
61
+ # Get providers stored in this data directory
62
+ #
63
+ # @param create [Boolean] Should the data be created if it does not exist?
64
+ # @return [Providers, nil] Providers stored in this data directory, or nil if none
65
+ def providers(create: self.create)
66
+ @providers ||= Providers.from_cline_data(dir, create:)
67
+ end
68
+
69
+ # Get secrets stored in this data directory
70
+ #
71
+ # @param create [Boolean] Should the data be created if it does not exist?
72
+ # @return [Secrets, nil] Secrets stored in this data directory, or nil if none
73
+ def secrets(create: self.create)
74
+ @secrets ||= Secrets.from_cline_data(dir, create:)
75
+ end
76
+
77
+ # Get sessions from this data directory
78
+ #
79
+ # @param cline_models [Models] The Cline models used to interpret the sessions' messages
80
+ # @param create [Boolean] Should the data be created if it does not exist?
81
+ # @return [Sessions] Set of sessions associated to this data directory
82
+ def sessions(cline_models: self.cline_models, create: self.create)
83
+ @sessions ||= Sessions.open(subpath('sessions'), cline_models:, create:)
84
+ end
85
+
86
+ # Get tasks from this data directory
87
+ #
88
+ # @param cline_models [Models] The Cline models used to interpret the tasks' messages
89
+ # @param create [Boolean] Should the data be created if it does not exist?
90
+ # @return [Tasks] Set of tasks associated to this data directory
91
+ def tasks(cline_models: self.cline_models, create: self.create)
92
+ @tasks ||= Tasks.open(subpath('tasks'), cline_models:, create:)
93
+ end
94
+
95
+ # Get workspaces from this data directory
96
+ #
97
+ # @param create [Boolean] Should the data be created if it does not exist?
98
+ # @return [Workspaces] Set of workspaces associated to this data directory
99
+ def workspaces(create: self.create)
100
+ @workspaces ||= Workspaces.open(subpath('workspaces'), create:)
101
+ end
102
+
103
+ # Equality check
104
+ #
105
+ # @param other [Object] The other to check equality with
106
+ # @return [Boolean] True if objects are equal
107
+ def ==(other)
108
+ other.is_a?(Data) &&
109
+ other.tasks == tasks &&
110
+ other.workspaces == workspaces &&
111
+ other.cline_models == cline_models &&
112
+ other.global_settings == global_settings &&
113
+ other.global_state == global_state &&
114
+ other.mcp_settings == mcp_settings &&
115
+ other.providers == providers &&
116
+ other.secrets == secrets
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,33 @@
1
+ module Cline
2
+ # Store a file's content, either from an existing file or just in memory for later persistence.
3
+ class FileContent
4
+ # @!group Public API
5
+
6
+ # Retrieve the file's content
7
+ #
8
+ # @return [String] The file's content
9
+ def content
10
+ @content ||= File.read(file)
11
+ end
12
+
13
+ # Equality check
14
+ #
15
+ # @param other [Object] The other to check equality with
16
+ # @return [Boolean] True if objects are equal
17
+ def ==(other)
18
+ other.is_a?(FileContent) &&
19
+ other.content == content
20
+ end
21
+
22
+ # @!group Internal
23
+
24
+ include Serializable::File
25
+
26
+ # Constructor
27
+ #
28
+ # @param content [String, nil] Content, or nil if the content is taken from a real file
29
+ def initialize(content = nil)
30
+ @content = content
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ module Cline
2
+ # Global settings for Cline
3
+ class GlobalSettings < Schema
4
+ # @!group Public API
5
+
6
+ Serializable::ClineData.include_for(self, 'settings/global-settings.json')
7
+
8
+ # @return [Boolean] Flag indicating if automatic updates are enabled
9
+ attribute :auto_update_enabled, :boolean
10
+
11
+ # @return [Boolean] Flag indicating if telemetry is opted out
12
+ attribute :telemetry_opt_out, :boolean
13
+
14
+ # @return [Array<String>] List of tools that are disabled
15
+ attribute :disabled_tools, Utils::Schema.collection(:string)
16
+ end
17
+ end