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
-
.anthropic_definition ⇒ Hash
Anthropic-compatible tool definition.
-
.call(command:, timeout: 30) ⇒ String
Execute a command without abort signal support.
-
.call_with_signal(command:, timeout: 30, signal: nil) ⇒ String
Execute a command with abort signal support.
-
.definition ⇒ Hash
OpenAI-compatible tool definition.
-
.on_update ⇒ Proc?
Current update callback.
-
.on_update=(callback) ⇒ Object
Register a callback for streaming execution updates.
- .strip_ansi_codes(text) ⇒ Object private
Class Method Details
.anthropic_definition ⇒ Hash
Returns 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.
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.
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.}" end end |
.definition ⇒ Hash
Returns 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_update ⇒ Proc?
Returns 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.
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 |