Module: Crimson::Tools::RunCommand

Defined in:
lib/crimson/tools/run_command.rb

Overview

Execute shell commands with timeout, streaming output, and abort support.

Constant Summary collapse

TOOL_NAME =
"run_command"
EXECUTION_MODE =

This tool must run sequentially (not parallel).

:sequential
PARAMS =

Tool parameter definitions.

{
  command: { type: "string", description: "The shell command to execute" },
  timeout: { type: "integer", description: "Timeout in seconds (default: 30)" }
}.freeze

Class Method Summary collapse

Class Method Details

.anthropic_definitionHash

Returns Anthropic-compatible tool definition.

Returns:

  • (Hash)

    Anthropic-compatible tool definition



42
43
44
# File 'lib/crimson/tools/run_command.rb', line 42

def self.anthropic_definition
  Schema.build_anthropic(name: TOOL_NAME, description: "Execute a shell command and return stdout and stderr.", parameters: PARAMS, required: ["command"])
end

.call(command:, timeout: 30) ⇒ String

Execute a command without abort signal support.

Parameters:

  • command (String)

    shell command

  • timeout (Integer) (defaults to: 30)

    timeout in seconds

Returns:

  • (String)

    command output or error



50
51
52
# File 'lib/crimson/tools/run_command.rb', line 50

def self.call(command:, timeout: 30)
  call_with_signal(command: command, timeout: timeout, signal: nil)
end

.call_with_signal(command:, timeout: 30, signal: nil) ⇒ String

Execute a command with abort signal support.

Parameters:

  • command (String)

    shell command

  • timeout (Integer) (defaults to: 30)

    timeout in seconds

  • signal (AbortSignal, nil) (defaults to: nil)

Returns:

  • (String)

    command output or error



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/crimson/tools/run_command.rb', line 59

def self.call_with_signal(command:, timeout: 30, signal: nil)
  return "Error: No command provided" if command.nil? || command.strip.empty?

  stdout = String.new
  stderr = String.new
  status = nil
  start_time = Time.now

  begin
    Timeout.timeout(timeout) do
      Open3.popen3(command) do |stdin, out, err, wait_thr|
        stdin.close

        abort_thread = if signal
          Thread.new do
            sleep 0.1 until signal.aborted? || !wait_thr.status
            if signal.aborted? && wait_thr.pid
              begin
                Process.kill("TERM", wait_thr.pid)
              rescue Errno::ESRCH, Errno::EPERM
              end
            end
          end
        end

        readers = [out, err]
        while readers.any?
          ready = IO.select(readers, nil, nil, 0.1)
          next unless ready

          ready[0].each do |io|
            chunk = io.read_nonblock(4096, exception: false)
            if chunk == :wait_readable || chunk.nil?
              readers.delete(io) if io.eof?
              next
            end
            if io == out
              stdout << chunk
            else
              stderr << chunk
            end
            elapsed = Time.now - start_time
            cb = on_update
            cb&.call(command, elapsed, stdout.length + stderr.length)
          end
        end

        status = wait_thr.value
        abort_thread&.kill
      end
    end

    output = String.new
    output << stdout if !stdout.empty?
    output << stderr if !stderr.empty?

    output = strip_ansi_codes(output)
    output = String.new("(no output)") if output.strip.empty?
    if status.success?
      # No exit code line needed for success
    elsif status.exitstatus
      output << "\n(exit code: #{status.exitstatus})"
    else
      output << "\n(process killed)"
    end
    output
  rescue Timeout::Error
    "Error: Command timed out after #{timeout} seconds"
  rescue => e
    "Error executing command: #{e.message}"
  end
end

.definitionHash

Returns OpenAI-compatible tool definition.

Returns:

  • (Hash)

    OpenAI-compatible tool definition



37
38
39
# File 'lib/crimson/tools/run_command.rb', line 37

def self.definition
  Schema.build(name: TOOL_NAME, description: "Execute a shell command and return stdout and stderr.", parameters: PARAMS, required: ["command"])
end

.on_updateProc?

Returns current update callback.

Returns:

  • (Proc, nil)

    current update callback



31
32
33
# File 'lib/crimson/tools/run_command.rb', line 31

def on_update
  @callback_mutex.synchronize { @update_callback }
end

.on_update=(callback) ⇒ Object

Register a callback for streaming execution updates.

Parameters:

  • callback (Proc, nil)


26
27
28
# File 'lib/crimson/tools/run_command.rb', line 26

def on_update=(callback)
  @callback_mutex.synchronize { @update_callback = callback }
end

.strip_ansi_codes(text) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



133
134
135
# File 'lib/crimson/tools/run_command.rb', line 133

def self.strip_ansi_codes(text)
  text.gsub(/\e\[[0-9;]*[a-zA-Z]/, '')
end