Class: Cline::Cli

Inherits:
Object
  • Object
show all
Includes:
Utils::Logger
Defined in:
lib/cline/cli.rb

Overview

Provide a wrapper over the Cline CLI tool

Defined Under Namespace

Classes: UnexpectedExitStatusError, UnexpectedInteractiveSessionError, UnknownOptionError

Constant Summary collapse

COMMANDS =

Define all commands and their options

{
  # Global options
  global: {
    verbose: '--verbose',
    cwd: '--cwd STRING',
    config: '--config STRING'
  },
  auth: {
    # Provider ID for quick setup (e.g., openai-native, anthropic, moonshot)
    provider: '--provider STRING',
    # API key for the provider
    apikey: '--apikey STRING',
    # Model ID to configure (e.g., gpt-4o, claude-sonnet-4-6, kimi-k2.5)
    modelid: '--modelid STRING',
    # Base URL (optional, only for openai provider)
    baseurl: '--baseurl STRING'
  },
  task: {
    # Run in plan mode
    plan: '--plan',
    # Output messages as JSON instead of styled text
    json: '--json',
    # Enable auto-approve all actions
    auto_approve: '--auto-approve',
    # Reasoning effort level between none|low|medium|high|xhigh
    thinking: '--thinking STRING',
    # Context compaction mode: agentic|basic|off
    compaction: '--compaction STRIG',
    # Open the terminal user interface (TUI) for interactive sessions
    tui: '--tui',
    # Session ID to resume, or nil for a new session
    id: '--id STRING',
    # Provider to use for the session
    provider: '--provider STRING',
    # API key to use for the session
    key: '--key STRING',
    # Model to use for the task
    model: '--model STRING',
    # Override the default system prompt
    system: '--system STRING',
    # Start a session that runs in the background hub
    zen: '--zen',
    # Number of maximum consecutive mistakes (retries) before exiting
    retries: '--retries INTEGER',
    # Optional timeout in seconds (applies only when provided)
    timeout: '--timeout INTEGER',
    # Run in Agent Client Protocol (ACP) mode for editor integration
    acp: '--acp',
    # Use isolated local state at this directory path
    data_dir: '--data-dir STRING',
    # Path to additional hooks directory for runtime hook injection
    hooks_dir: '--hooks-dir STRING',
    # Auto-create a detached git worktree under ~/.cline/worktrees/ and run the task there
    worktree: '--worktree',
    # Run the kanban app
    kanban: '--kanban'
  }
}

Public API collapse

Public API collapse

Methods included from Utils::Logger

#log_debug, sanitize_pty_output

Constructor Details

#initialize(stdout_echo: false, **kwargs) ⇒ Cli

Constructor

Parameters:

  • stdout_echo (Boolean) (defaults to: false)

    Do we echo stdout of Cline CLI?

  • kwargs (Hash{Symbol => Object})

    Global options (see COMMANDS)



92
93
94
95
96
97
98
99
100
101
102
# File 'lib/cline/cli.rb', line 92

def initialize(stdout_echo: false, **kwargs)
  @global_options = parse_global_options(**kwargs)
  @stdout_echo = stdout_echo
  @config_dir = kwargs[:config]
  @cline_pid = nil
  # [Session] Session associated to this CLI run.
  # The session is discovered using:
  # 1. Cline logs appearing after executing CLI that contain a session ID.
  # 2. The session corresponding to this ID.
  @session = nil
end

Instance Attribute Details

#cline_pidInteger? (readonly)

Returns The PID of the running Cline process, if any.

Returns:

  • (Integer, nil)

    The PID of the running Cline process, if any



241
242
243
# File 'lib/cline/cli.rb', line 241

def cline_pid
  @cline_pid
end

#sessionSession? (readonly)

Returns The current or last session handled by the Cline process, if any.

Returns:

  • (Session, nil)

    The current or last session handled by the Cline process, if any



244
245
246
# File 'lib/cline/cli.rb', line 244

def session
  @session
end

Instance Method Details

#auth(**kwargs) ⇒ Hash{Symbol => Object}

Authenticate the CLI

Parameters:

  • kwargs (Hash{Symbol => Object})

    Command options (see COMMANDS)

Returns:

  • (Hash{Symbol => Object})

    A set of return properties (see #run_cli)



108
109
110
# File 'lib/cline/cli.rb', line 108

def auth(**kwargs)
  run_cli(command: 'auth', args: parse_auth_options(**kwargs))
end

#interruptObject

Interrupt the running Cline command



247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/cline/cli.rb', line 247

def interrupt
  if cline_pid
    log_debug "Interrupt current command with PID #{cline_pid}"
    all_pids = [cline_pid] + get_child_pids_recursive(cline_pid)
    log_debug "Found process tree PIDs: #{all_pids.join(', ')}"
    @interrupted_on_purpose = true
    all_pids.reverse.each do |pid|
      log_debug "Kill process #{pid}"
      Utils::Os.kill(pid)
    end
  else
    log_debug 'No Cline command started, so no need to interrupt anything'
  end
end

#task(prompt, on_message: nil, on_question: nil, monitoring_interval_secs: 1, **kwargs) ⇒ Hash{Symbol => Object}

Start a task by sending a prompt

Parameters:

  • prompt (String, nil)

    The prompt, or nil if none

  • on_message (#call, nil) (defaults to: nil)

    Callback called for each new or updated message for the session of this prompt, or nil if none (see Session#monitor_message)

  • on_question (#call, nil) (defaults to: nil)

    Callback called for each question that is asked by the assistant, or nil if none. This should be set if an interactive session is expected. If a question is asked without a callback to handle it, an UnexpectedInteractiveSessionError exceptioon will be raised.

    • Param question [SessionMessage::MessageContent::ToolUseInput] Question input with possible options (see SessionMessage::MessageContent::ToolUseInput).
    • Return [String] The answer that the user should provide to this assistant's question.
  • monitoring_interval_secs (Float) (defaults to: 1)

    The monitoring interval in seconds

  • kwargs (Hash{Symbol => Object})

    Command options (see COMMANDS)

Returns:

  • (Hash{Symbol => Object})

    A set of return properties (see #run_cli). Additionnally the following ones:

    • message [SessionMessage, nil] The last message of the session, or nil if none
    • status [String] The task status
    • error [Log, nil] In case of status "failed", get the last error log entry, or nil if no error.


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/cline/cli.rb', line 129

def task(prompt, on_message: nil, on_question: nil, monitoring_interval_secs: 1, **kwargs)
  result = {}
  start_time = Time.now
  stripped_prompt = prompt&.strip
  task_args = parse_task_options(**kwargs)
  # Use a prompt temporary file if the prompt is too long to fit in the command line.
  (
    if stripped_prompt && stripped_prompt.size + 2 > Utils::Os.max_cmd_length - (Utils::Os.cline_exe + task_args).map { |arg| "\"#{arg}\"" }.join(' ').size
      proc do |&run_block|
        Utils::File.with_temp_dir do |tmp_dir|
          prompt_file = "#{tmp_dir}/prompt.txt"
          File.write(prompt_file, stripped_prompt)
          run_block.call(
            "The user prompt is given to you in the file `#{File.expand_path(prompt_file)}`. " \
              'You have to read it and treat its content as the user prompt.'
          )
        end
      end
    else
      proc { |&run_block| run_block.call(stripped_prompt) }
    end
  ).call do |prompt_arg|
    session_monitor_thread = nil
    cli_running = true
    begin
      result = run_cli(
        args: task_args + (prompt_arg ? [prompt_arg] : []),
        on_start: proc do |_reader, writer, _pid|
          session_monitor_thread = Thread.new do
            # Start monitoring logs to get the session ID.
            # Wait for config and logs to exist.
            sleep 0.1 while cli_running && !config&.logs
            # Monitor logs to get the session ID
            session_id = nil
            config&.logs&.monitor(
              on_log: proc do |log, _last|
                session_id = log.properties.ulid if !session_id && log.is_a?(Log) && log.properties&.ulid
              end,
              from: start_time
            ) do
              sleep 0.1 while cli_running && !session_id
            end
            # If CLI has finished, session_id should be already discovered, unless the session could not be created.
            # If CLI has not finished yet, the we have the session ID discovered already.
            # So it means that if session_id is nil, there has been a problem (like core dump).
            if session_id
              log_debug "Found Cline session ID #{session_id}"
              # First get Cline models
              cline_models = config.data&.cline_models || Data.vscode&.cline_models
              # Wait for the CLI to create the session for real
              sleep 0.1 while cli_running && !config.sessions(cline_models:)
              config.sessions(cline_models:) unless cli_running
              while cli_running && !config.sessions[session_id]
                sleep 0.1
                config.sessions.refresh!
              end
              # If CLI has finished, then the session should exist, unless there has been a problem (like file system issue).
              @session = config.sessions && config.sessions[session_id]
              # Now monitor the session messages for reporting and possible user callback
              # [Hash{Integer => Usage}] All usages, per timestamp, for logging purposes
              usages = {}
              @session&.monitor_messages(
                on_message: proc do |message, last, previous_version|
                  log_debug do
                    usages[message.ts] = message.usage if message.usage
                    last_usage = usages.values.last
                    prefix = "[#{message.timestamp.strftime('%H:%M:%S')}]#{
                      unless last_usage.nil?
                        " (#{HumanNumber.currency(usages.values.map { |usage| usage.cost || 0.0 }.sum, currency_code: 'USD')} " \
                          "#{HumanNumber.human_number(last_usage.context_tokens, max_digits: 2)}" \
                          "/#{HumanNumber.human_number(last_usage.context_tokens_limit || 0, max_digits: 2)})"
                      end
                    } - "
                    "#{prefix}#{message.to_human(limit: 128 - prefix.size)}"
                  end
                  # Call the user callback if any
                  on_message&.call(message, last, previous_version)
                  # If the message is the last one and the agent has asked a question, call the corresponding callback
                  if last
                    last_content = message.content&.last
                    if last_content&.type == 'tool_use' && last_content.name == 'ask_question'
                      unless on_question
                        raise UnexpectedInteractiveSessionError,
                          "Unexpected interactive session with assistant asking question #{last_content.input&.question}"
                      end

                      writer.puts(on_question.call(last_content.input))
                    end
                  end
                end,
                monitoring_interval_secs:
              ) do
                sleep 0.1 while cli_running
              end
            end
          end
        end
      )
    ensure
      cli_running = false
      session_monitor_thread&.join
    end
  end
  if @session
    result[:message] = @session.messages&.reverse_each&.find { |message| message.role == 'assistant' }
    result[:status] = @session.status
    result[:error] = config.logs.logs(from: start_time).reverse_each.find { |log| log.severity == 'error' } if @session.status == 'failed'
  end
  result
end