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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +139 -0
- data/README.md +1216 -0
- data/TODO.md +2 -0
- data/lib/cline/cli.rb +373 -0
- data/lib/cline/config.rb +100 -0
- data/lib/cline/configuration.rb +23 -0
- data/lib/cline/data.rb +119 -0
- data/lib/cline/file_content.rb +33 -0
- data/lib/cline/global_settings.rb +17 -0
- data/lib/cline/global_state/api_providers.rb +48 -0
- data/lib/cline/global_state/auto_approval.rb +73 -0
- data/lib/cline/global_state/browser.rb +52 -0
- data/lib/cline/global_state/features.rb +56 -0
- data/lib/cline/global_state/general.rb +77 -0
- data/lib/cline/global_state/models.rb +127 -0
- data/lib/cline/global_state/toggles.rb +33 -0
- data/lib/cline/global_state/workspace.rb +41 -0
- data/lib/cline/global_state.rb +16 -0
- data/lib/cline/log.rb +288 -0
- data/lib/cline/logs.rb +136 -0
- data/lib/cline/mcp_settings.rb +30 -0
- data/lib/cline/model.rb +47 -0
- data/lib/cline/models.rb +11 -0
- data/lib/cline/overlay_hash.rb +125 -0
- data/lib/cline/providers.rb +59 -0
- data/lib/cline/schema.rb +144 -0
- data/lib/cline/secret_string.rb +83 -0
- data/lib/cline/secrets.rb +119 -0
- data/lib/cline/serializable/cline_data.rb +131 -0
- data/lib/cline/serializable/dir.rb +81 -0
- data/lib/cline/serializable/file.rb +106 -0
- data/lib/cline/session.rb +87 -0
- data/lib/cline/session_data.rb +154 -0
- data/lib/cline/session_message.rb +178 -0
- data/lib/cline/session_messages.rb +61 -0
- data/lib/cline/sessions.rb +30 -0
- data/lib/cline/skill.rb +148 -0
- data/lib/cline/skills.rb +8 -0
- data/lib/cline/task.rb +75 -0
- data/lib/cline/task_message.rb +247 -0
- data/lib/cline/task_messages.rb +11 -0
- data/lib/cline/tasks.rb +30 -0
- data/lib/cline/usage.rb +37 -0
- data/lib/cline/utils/enumerable_dir_objects.rb +103 -0
- data/lib/cline/utils/file.rb +71 -0
- data/lib/cline/utils/file_monitor.rb +56 -0
- data/lib/cline/utils/logger.rb +37 -0
- data/lib/cline/utils/os/linux.rb +43 -0
- data/lib/cline/utils/os/mingw32.rb +46 -0
- data/lib/cline/utils/os.rb +31 -0
- data/lib/cline/utils/schema.rb +290 -0
- data/lib/cline/version.rb +6 -0
- data/lib/cline/workspace.rb +25 -0
- data/lib/cline/workspace_settings.rb +29 -0
- data/lib/cline/workspaces.rb +8 -0
- data/lib/cline.rb +22 -0
- metadata +249 -0
data/TODO.md
ADDED
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
|
data/lib/cline/config.rb
ADDED
|
@@ -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
|