ruby_llm-toolbox 0.1.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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +49 -0
  3. data/GUIDE.md +598 -0
  4. data/LICENSE +21 -0
  5. data/README.md +412 -0
  6. data/bin/verify_prism_parity +112 -0
  7. data/lib/ruby_llm/toolbox/base.rb +112 -0
  8. data/lib/ruby_llm/toolbox/configuration.rb +148 -0
  9. data/lib/ruby_llm/toolbox/data_path.rb +54 -0
  10. data/lib/ruby_llm/toolbox/process_registry.rb +226 -0
  11. data/lib/ruby_llm/toolbox/process_runner.rb +72 -0
  12. data/lib/ruby_llm/toolbox/ruby_outline.rb +213 -0
  13. data/lib/ruby_llm/toolbox/safe_math.rb +182 -0
  14. data/lib/ruby_llm/toolbox/safety/command_guard.rb +42 -0
  15. data/lib/ruby_llm/toolbox/safety/path_jail.rb +55 -0
  16. data/lib/ruby_llm/toolbox/safety/url_guard.rb +111 -0
  17. data/lib/ruby_llm/toolbox/sandbox/base.rb +151 -0
  18. data/lib/ruby_llm/toolbox/sandbox/bubblewrap.rb +70 -0
  19. data/lib/ruby_llm/toolbox/sandbox/docker.rb +69 -0
  20. data/lib/ruby_llm/toolbox/sandbox/sandbox_exec.rb +75 -0
  21. data/lib/ruby_llm/toolbox/search/brave.rb +64 -0
  22. data/lib/ruby_llm/toolbox/search/searxng.rb +64 -0
  23. data/lib/ruby_llm/toolbox/search/tavily.rb +70 -0
  24. data/lib/ruby_llm/toolbox/text_diff.rb +81 -0
  25. data/lib/ruby_llm/toolbox/toml.rb +409 -0
  26. data/lib/ruby_llm/toolbox/tools/apply_patch.rb +92 -0
  27. data/lib/ruby_llm/toolbox/tools/bash_tool.rb +101 -0
  28. data/lib/ruby_llm/toolbox/tools/bundle.rb +71 -0
  29. data/lib/ruby_llm/toolbox/tools/calculator.rb +42 -0
  30. data/lib/ruby_llm/toolbox/tools/create_directory.rb +35 -0
  31. data/lib/ruby_llm/toolbox/tools/csv_read.rb +69 -0
  32. data/lib/ruby_llm/toolbox/tools/csv_write.rb +51 -0
  33. data/lib/ruby_llm/toolbox/tools/date_time.rb +42 -0
  34. data/lib/ruby_llm/toolbox/tools/delete_file.rb +64 -0
  35. data/lib/ruby_llm/toolbox/tools/diff.rb +35 -0
  36. data/lib/ruby_llm/toolbox/tools/download_file.rb +55 -0
  37. data/lib/ruby_llm/toolbox/tools/edit_file.rb +82 -0
  38. data/lib/ruby_llm/toolbox/tools/gem_tool.rb +140 -0
  39. data/lib/ruby_llm/toolbox/tools/git_add.rb +46 -0
  40. data/lib/ruby_llm/toolbox/tools/git_blame.rb +58 -0
  41. data/lib/ruby_llm/toolbox/tools/git_branch.rb +35 -0
  42. data/lib/ruby_llm/toolbox/tools/git_checkout.rb +43 -0
  43. data/lib/ruby_llm/toolbox/tools/git_commit.rb +47 -0
  44. data/lib/ruby_llm/toolbox/tools/git_diff.rb +50 -0
  45. data/lib/ruby_llm/toolbox/tools/git_grep.rb +66 -0
  46. data/lib/ruby_llm/toolbox/tools/git_helpers.rb +68 -0
  47. data/lib/ruby_llm/toolbox/tools/git_log.rb +47 -0
  48. data/lib/ruby_llm/toolbox/tools/git_show.rb +48 -0
  49. data/lib/ruby_llm/toolbox/tools/git_status.rb +27 -0
  50. data/lib/ruby_llm/toolbox/tools/glob.rb +62 -0
  51. data/lib/ruby_llm/toolbox/tools/grep_files.rb +221 -0
  52. data/lib/ruby_llm/toolbox/tools/http_helpers.rb +130 -0
  53. data/lib/ruby_llm/toolbox/tools/http_request.rb +75 -0
  54. data/lib/ruby_llm/toolbox/tools/json_query.rb +69 -0
  55. data/lib/ruby_llm/toolbox/tools/lint.rb +67 -0
  56. data/lib/ruby_llm/toolbox/tools/list_directory.rb +87 -0
  57. data/lib/ruby_llm/toolbox/tools/move_file.rb +54 -0
  58. data/lib/ruby_llm/toolbox/tools/multi_edit.rb +107 -0
  59. data/lib/ruby_llm/toolbox/tools/parse_ruby.rb +111 -0
  60. data/lib/ruby_llm/toolbox/tools/process_kill.rb +41 -0
  61. data/lib/ruby_llm/toolbox/tools/process_list.rb +29 -0
  62. data/lib/ruby_llm/toolbox/tools/process_output.rb +55 -0
  63. data/lib/ruby_llm/toolbox/tools/process_start.rb +109 -0
  64. data/lib/ruby_llm/toolbox/tools/python_tests.rb +77 -0
  65. data/lib/ruby_llm/toolbox/tools/read_file.rb +75 -0
  66. data/lib/ruby_llm/toolbox/tools/replace_in_files.rb +139 -0
  67. data/lib/ruby_llm/toolbox/tools/run_python.rb +38 -0
  68. data/lib/ruby_llm/toolbox/tools/run_ruby.rb +37 -0
  69. data/lib/ruby_llm/toolbox/tools/run_rust.rb +42 -0
  70. data/lib/ruby_llm/toolbox/tools/run_tests.rb +81 -0
  71. data/lib/ruby_llm/toolbox/tools/sandbox_run.rb +40 -0
  72. data/lib/ruby_llm/toolbox/tools/todo_write.rb +57 -0
  73. data/lib/ruby_llm/toolbox/tools/toml_query.rb +70 -0
  74. data/lib/ruby_llm/toolbox/tools/toolchain_helpers.rb +62 -0
  75. data/lib/ruby_llm/toolbox/tools/tree.rb +87 -0
  76. data/lib/ruby_llm/toolbox/tools/web_fetch.rb +77 -0
  77. data/lib/ruby_llm/toolbox/tools/web_search.rb +81 -0
  78. data/lib/ruby_llm/toolbox/tools/write_file.rb +52 -0
  79. data/lib/ruby_llm/toolbox/tools/yaml_query.rb +73 -0
  80. data/lib/ruby_llm/toolbox/truncator.rb +68 -0
  81. data/lib/ruby_llm/toolbox/version.rb +7 -0
  82. data/lib/ruby_llm/toolbox.rb +161 -0
  83. metadata +194 -0
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm/toolbox/base"
4
+ require "ruby_llm/toolbox/tools/http_helpers"
5
+
6
+ module RubyLLM
7
+ module Toolbox
8
+ module Tools
9
+ # SAFE. Fetches a URL over http/https and returns its readable text (HTML
10
+ # is stripped to text). Every request and redirect hop passes through
11
+ # UrlGuard, so it can't be pointed at internal/loopback/metadata addresses.
12
+ # Output is token-budgeted.
13
+ class WebFetch < Base
14
+ include HttpHelpers
15
+
16
+ description "Fetch a web page or text/JSON resource over http/https and return its readable " \
17
+ "content (HTML is converted to text). Follows redirects safely. Cannot reach " \
18
+ "private, loopback, or cloud-metadata addresses."
19
+
20
+ param :url, type: "string",
21
+ desc: "The http/https URL to fetch.",
22
+ required: true
23
+ param :unsafe, type: "boolean",
24
+ desc: "Request bypassing SSRF protection (UrlGuard). Only takes effect if an " \
25
+ "operator enabled allow_unsafe; otherwise the call is refused. Default false.",
26
+ required: false
27
+
28
+ def execute(url:, unsafe: false)
29
+ bypass = permit_unsafe!(unsafe, url)
30
+ response = guarded_get(url, guard: !bypass)
31
+
32
+ if response.status >= 400
33
+ return error("HTTP #{response.status} from #{response.final_url}", code: :http_error)
34
+ end
35
+
36
+ content_type = content_type_of(response)
37
+ text = extract_text(response.body, content_type)
38
+
39
+ header = "GET #{response.final_url} -> #{response.status} (#{content_type})"
40
+ truncate("#{header}\n\n#{text}".strip)
41
+ rescue Safety::UrlGuard::Blocked => e
42
+ error(e.message, code: :url_blocked)
43
+ rescue HttpHelpers::FetchError => e
44
+ error("fetch failed: #{e.message}", code: :fetch_failed)
45
+ end
46
+
47
+ private
48
+
49
+ def content_type_of(response)
50
+ Array(response.headers["content-type"]).first.to_s.split(";").first.to_s.strip
51
+ end
52
+
53
+ def extract_text(body, content_type)
54
+ text = body.to_s.scrub
55
+ return collapse(text) unless content_type =~ /html|xml/i
56
+
57
+ text = text.dup
58
+ text.gsub!(%r{<script\b[^>]*>.*?</script>}mi, " ")
59
+ text.gsub!(%r{<style\b[^>]*>.*?</style>}mi, " ")
60
+ text.gsub!(/<!--.*?-->/m, " ")
61
+ text.gsub!(%r{</(?:p|div|br|li|h[1-6]|tr|section|article)\s*/?>}i, "\n")
62
+ text.gsub!(/<[^>]+>/, " ")
63
+ collapse(decode_entities(text))
64
+ end
65
+
66
+ def decode_entities(str)
67
+ str.gsub("&amp;", "&").gsub("&lt;", "<").gsub("&gt;", ">")
68
+ .gsub("&quot;", '"').gsub("&#39;", "'").gsub("&nbsp;", " ")
69
+ end
70
+
71
+ def collapse(str)
72
+ str.gsub(/[ \t]+/, " ").gsub(/\n[ \t]*/, "\n").gsub(/\n{3,}/, "\n\n").strip
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm/toolbox/base"
4
+ require "ruby_llm/toolbox/search/tavily"
5
+ require "ruby_llm/toolbox/search/brave"
6
+ require "ruby_llm/toolbox/search/searxng"
7
+
8
+ module RubyLLM
9
+ module Toolbox
10
+ module Tools
11
+ # SAFE. Searches the web through the configured adapter (Tavily by
12
+ # default) and returns ranked results plus an optional synthesized answer.
13
+ # Requires an API key for the default Tavily adapter (config.tavily_api_key).
14
+ class WebSearch < Base
15
+ description "Search the web and return ranked results (title, URL, snippet) with an optional " \
16
+ "synthesized answer. Uses Tavily by default (needs config.tavily_api_key); switch " \
17
+ "providers with config.search_adapter set to :brave, :searxng, or a custom adapter."
18
+
19
+ param :query, type: "string",
20
+ desc: "The search query.",
21
+ required: true
22
+ param :max_results, type: "integer",
23
+ desc: "Number of results to return (1-10, default 5).",
24
+ required: false
25
+
26
+ def execute(query:, max_results: 5)
27
+ q = query.to_s.strip
28
+ return error("query must not be empty", code: :bad_query) if q.empty?
29
+
30
+ n = max_results.to_i
31
+ n = 5 if n <= 0
32
+ n = 10 if n > 10
33
+
34
+ result = adapter.search(q, max_results: n)
35
+ truncate(format_results(q, result))
36
+ rescue Search::Error => e
37
+ code = e.message.match?(/missing .*(api key|url)/i) ? :no_api_key : :search_failed
38
+ error(e.message, code: code)
39
+ end
40
+
41
+ private
42
+
43
+ # config.search_adapter may be: nil (→ Tavily), a Symbol naming a built-in
44
+ # adapter, or any object that responds to #search(query, max_results:).
45
+ def adapter
46
+ case config.search_adapter
47
+ when nil, :tavily
48
+ Search::Tavily.new(api_key: config.tavily_api_key, user_agent: config.user_agent,
49
+ timeout: config.http_timeout)
50
+ when :brave
51
+ Search::Brave.new(api_key: config.brave_api_key, user_agent: config.user_agent,
52
+ timeout: config.http_timeout)
53
+ when :searxng
54
+ Search::SearXNG.new(base_url: config.searxng_url, user_agent: config.user_agent,
55
+ timeout: config.http_timeout)
56
+ when Symbol
57
+ raise Search::Error, "unknown search adapter #{config.search_adapter.inspect} " \
58
+ "(expected :tavily, :brave, :searxng, or an adapter object)"
59
+ else
60
+ config.search_adapter
61
+ end
62
+ end
63
+
64
+ def format_results(query, result)
65
+ results = Array(result[:results])
66
+ return "no results for #{query.inspect}" if results.empty?
67
+
68
+ body = +"Results for #{query.inspect}:\n"
69
+ results.each_with_index do |r, i|
70
+ body << "\n#{i + 1}. #{r[:title]}\n #{r[:url]}\n"
71
+ snippet = r[:content].to_s.strip
72
+ body << " #{snippet}\n" unless snippet.empty?
73
+ end
74
+ answer = result[:answer].to_s.strip
75
+ body << "\nAnswer: #{answer}\n" unless answer.empty?
76
+ body
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "ruby_llm/toolbox/base"
5
+ require "ruby_llm/toolbox/safety/path_jail"
6
+
7
+ module RubyLLM
8
+ module Toolbox
9
+ module Tools
10
+ # EXEC. Creates or overwrites a text file within fs_root. Missing parent
11
+ # directories are created (inside the jail). Path traversal and symlink
12
+ # escapes are rejected by PathJail.
13
+ class WriteFile < Base
14
+ exec_tool!
15
+
16
+ description "Create or overwrite a text file within fs_root. Missing parent directories " \
17
+ "are created automatically. Overwrites an existing file at the path."
18
+
19
+ param :path, type: "string",
20
+ desc: "File path relative to fs_root (or an absolute path inside it).",
21
+ required: true
22
+ param :content, type: "string",
23
+ desc: "Full contents to write. Replaces the file entirely.",
24
+ required: true
25
+ param :unsafe, type: "boolean",
26
+ desc: "Request writing outside fs_root (bypass the path jail). Only takes effect " \
27
+ "if an operator enabled allow_unsafe; otherwise the call is refused. Default false.",
28
+ required: false
29
+
30
+ MAX_BYTES = 10 * 1024 * 1024
31
+
32
+ def execute(path:, content:, unsafe: false)
33
+ body = content.to_s
34
+ return error("content too large (> #{MAX_BYTES} bytes)", code: :too_large) if body.bytesize > MAX_BYTES
35
+
36
+ jail = path_jail(unsafe: unsafe, detail: path)
37
+ real = jail.resolve(path)
38
+ return error("path is a directory: #{path}", code: :is_a_directory) if File.directory?(real)
39
+
40
+ existed = File.exist?(real)
41
+ FileUtils.mkdir_p(File.dirname(real))
42
+ File.write(real, body)
43
+
44
+ verb = existed ? "Overwrote" : "Created"
45
+ "#{verb} #{path} (#{body.bytesize} bytes)"
46
+ rescue Safety::PathJail::Jailbreak => e
47
+ error(e.message, code: :path_denied)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+ require "date"
6
+ require "ruby_llm/toolbox/base"
7
+ require "ruby_llm/toolbox/data_path"
8
+ require "ruby_llm/toolbox/safety/path_jail"
9
+
10
+ module RubyLLM
11
+ module Toolbox
12
+ module Tools
13
+ # SAFE. Parses YAML (from a file in fs_root or an inline string) and either
14
+ # pretty-prints it (as JSON, for readability) or extracts a value with a
15
+ # dot/bracket path. Uses safe_load — no arbitrary Ruby objects are
16
+ # instantiated from the document.
17
+ #
18
+ # Path syntax matches json_query: users[0].name, users[].email, config.port
19
+ class YamlQuery < Base
20
+ description "Query YAML with a path expression, or pretty-print it. Provide either a file path " \
21
+ "(within fs_root) or an inline yaml string. Loaded safely (no object deserialization). " \
22
+ "Path syntax: keys separated by dots, [n] for array index, [] to map over an array."
23
+
24
+ MAX_BYTES = 5 * 1024 * 1024
25
+ PERMITTED = [Date, Time, Symbol].freeze
26
+
27
+ param :path, type: "string",
28
+ desc: "YAML file to read, relative to fs_root. Provide this or yaml.",
29
+ required: false
30
+ param :yaml, type: "string",
31
+ desc: "Inline YAML string. Provide this or path.",
32
+ required: false
33
+ param :query, type: "string",
34
+ desc: "Path expression to extract, e.g. 'services[0].image'. Omit to pretty-print. Optional.",
35
+ required: false
36
+
37
+ def execute(path: nil, yaml: nil, query: nil)
38
+ source = load_source(path, yaml)
39
+ return source if source.is_a?(Hash) # error
40
+
41
+ data = YAML.safe_load(source, permitted_classes: PERMITTED, aliases: true)
42
+ result = query.to_s.strip.empty? ? data : DataPath.query(data, query)
43
+ truncate(JSON.pretty_generate(result))
44
+ rescue Safety::PathJail::Jailbreak => e
45
+ error(e.message, code: :path_denied)
46
+ rescue Psych::DisallowedClass => e
47
+ error("YAML contains a disallowed type: #{e.message}", code: :unsafe_yaml)
48
+ rescue Psych::SyntaxError => e
49
+ error("invalid YAML: #{e.message}", code: :bad_yaml)
50
+ rescue DataPath::Error => e
51
+ error(e.message, code: :bad_query)
52
+ end
53
+
54
+ private
55
+
56
+ def load_source(path, yaml)
57
+ if path && !path.to_s.empty?
58
+ jail = Safety::PathJail.new(config.fs_root)
59
+ real = jail.resolve(path)
60
+ return error("not a file: #{path}", code: :not_a_file) unless File.file?(real)
61
+ return error("file too large (> #{MAX_BYTES} bytes)", code: :too_large) if File.size(real) > MAX_BYTES
62
+
63
+ File.read(real).scrub
64
+ elsif yaml && !yaml.to_s.empty?
65
+ yaml.to_s
66
+ else
67
+ error("provide either path or yaml", code: :no_input)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm/tokenizer"
4
+
5
+ module RubyLLM
6
+ module Toolbox
7
+ # Token-budgets a string so a single tool result can never blow up the
8
+ # context window. Keeps the head and the tail (the two ends an agent most
9
+ # often needs) and elides the middle. Counting goes through
10
+ # ruby_llm-tokenizer; if no tokenizer backend is available for the model,
11
+ # it falls back to a ~4-chars-per-token heuristic so a tool never crashes
12
+ # just because a tokenizer gem isn't installed.
13
+ class Truncator
14
+ MARKER_RESERVE = 16 # tokens held back for the elision marker
15
+
16
+ def initialize(model:, max_tokens:)
17
+ @model = model
18
+ @max_tokens = [max_tokens.to_i, 1].max
19
+ end
20
+
21
+ def call(text)
22
+ text = text.to_s
23
+ total = count(text)
24
+ return text if total <= @max_tokens
25
+
26
+ budget = [@max_tokens - MARKER_RESERVE, 2].max
27
+ head_budget = (budget * 0.6).floor
28
+ tail_budget = budget - head_budget
29
+
30
+ head = take(text, head_budget, from: :head)
31
+ tail = take(text, tail_budget, from: :tail)
32
+ cut = total - count(head) - count(tail)
33
+
34
+ "#{head}\n\n…[truncated ~#{cut} tokens]…\n\n#{tail}"
35
+ end
36
+
37
+ private
38
+
39
+ def count(text)
40
+ RubyLLM::Tokenizer.count(text, model: @model)
41
+ rescue StandardError
42
+ (text.to_s.length / 4.0).ceil
43
+ end
44
+
45
+ # Longest prefix (or suffix) of `text` whose token count fits `max_tokens`.
46
+ # Binary search keeps this O(log n) tokenizer calls instead of O(n).
47
+ def take(text, max_tokens, from:)
48
+ return +"" if max_tokens <= 0
49
+ return text if count(text) <= max_tokens
50
+
51
+ lo = 0
52
+ hi = text.length
53
+ best = +""
54
+ while lo <= hi
55
+ mid = (lo + hi) / 2
56
+ slice = (from == :head ? text[0, mid] : text[text.length - mid, mid]).to_s
57
+ if count(slice) <= max_tokens
58
+ best = slice
59
+ lo = mid + 1
60
+ else
61
+ hi = mid - 1
62
+ end
63
+ end
64
+ best
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Toolbox
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+
5
+ require_relative "toolbox/version"
6
+ require_relative "toolbox/configuration"
7
+ require_relative "toolbox/truncator"
8
+ require_relative "toolbox/base"
9
+ require_relative "toolbox/process_runner"
10
+ require_relative "toolbox/process_registry"
11
+ require_relative "toolbox/ruby_outline"
12
+ require_relative "toolbox/safety/path_jail"
13
+ require_relative "toolbox/safety/command_guard"
14
+ require_relative "toolbox/safety/url_guard"
15
+ require_relative "toolbox/sandbox/base"
16
+ require_relative "toolbox/sandbox/docker"
17
+ require_relative "toolbox/sandbox/bubblewrap"
18
+ require_relative "toolbox/sandbox/sandbox_exec"
19
+ require_relative "toolbox/tools/read_file"
20
+ require_relative "toolbox/tools/list_directory"
21
+ require_relative "toolbox/tools/tree"
22
+ require_relative "toolbox/tools/glob"
23
+ require_relative "toolbox/tools/grep_files"
24
+ require_relative "toolbox/tools/gem_tool"
25
+ require_relative "toolbox/tools/parse_ruby"
26
+ require_relative "toolbox/tools/web_fetch"
27
+ require_relative "toolbox/tools/web_search"
28
+ require_relative "toolbox/tools/http_request"
29
+ require_relative "toolbox/tools/json_query"
30
+ require_relative "toolbox/tools/yaml_query"
31
+ require_relative "toolbox/tools/toml_query"
32
+ require_relative "toolbox/tools/csv_read"
33
+ require_relative "toolbox/tools/calculator"
34
+ require_relative "toolbox/tools/date_time"
35
+ require_relative "toolbox/tools/diff"
36
+ require_relative "toolbox/tools/todo_write"
37
+ require_relative "toolbox/tools/process_output"
38
+ require_relative "toolbox/tools/process_list"
39
+ require_relative "toolbox/tools/process_kill"
40
+ require_relative "toolbox/tools/git_helpers"
41
+ require_relative "toolbox/tools/git_status"
42
+ require_relative "toolbox/tools/git_diff"
43
+ require_relative "toolbox/tools/git_log"
44
+ require_relative "toolbox/tools/git_show"
45
+ require_relative "toolbox/tools/git_blame"
46
+ require_relative "toolbox/tools/git_grep"
47
+ require_relative "toolbox/tools/git_branch"
48
+ require_relative "toolbox/tools/write_file"
49
+ require_relative "toolbox/tools/edit_file"
50
+ require_relative "toolbox/tools/multi_edit"
51
+ require_relative "toolbox/tools/create_directory"
52
+ require_relative "toolbox/tools/move_file"
53
+ require_relative "toolbox/tools/delete_file"
54
+ require_relative "toolbox/tools/git_add"
55
+ require_relative "toolbox/tools/git_commit"
56
+ require_relative "toolbox/tools/git_checkout"
57
+ require_relative "toolbox/tools/apply_patch"
58
+ require_relative "toolbox/tools/csv_write"
59
+ require_relative "toolbox/tools/replace_in_files"
60
+ require_relative "toolbox/tools/download_file"
61
+ require_relative "toolbox/tools/toolchain_helpers"
62
+ require_relative "toolbox/tools/run_tests"
63
+ require_relative "toolbox/tools/python_tests"
64
+ require_relative "toolbox/tools/lint"
65
+ require_relative "toolbox/tools/bundle"
66
+ require_relative "toolbox/tools/sandbox_run"
67
+ require_relative "toolbox/tools/bash_tool"
68
+ require_relative "toolbox/tools/process_start"
69
+ require_relative "toolbox/tools/run_ruby"
70
+ require_relative "toolbox/tools/run_python"
71
+ require_relative "toolbox/tools/run_rust"
72
+
73
+ module RubyLLM
74
+ # A safe-by-default bundle of RubyLLM::Tool classes covering the skills common
75
+ # to most LLM harnesses. One require loads everything; the dangerous tools are
76
+ # inert until you flip config.enable_exec_tools.
77
+ module Toolbox
78
+ class << self
79
+ def config
80
+ @config ||= Configuration.new
81
+ end
82
+
83
+ def configure
84
+ yield config
85
+ end
86
+
87
+ # Resets global config. Primarily for tests.
88
+ def reset!
89
+ @config = nil
90
+ end
91
+
92
+ # Always-on, read-only tools.
93
+ def safe_tools(**overrides)
94
+ [
95
+ Tools::ReadFile.new(**overrides),
96
+ Tools::ListDirectory.new(**overrides),
97
+ Tools::Tree.new(**overrides),
98
+ Tools::Glob.new(**overrides),
99
+ Tools::GrepFiles.new(**overrides),
100
+ Tools::GemTool.new(**overrides),
101
+ Tools::ParseRuby.new(**overrides),
102
+ Tools::WebFetch.new(**overrides),
103
+ Tools::WebSearch.new(**overrides),
104
+ Tools::HttpRequest.new(**overrides),
105
+ Tools::GitStatus.new(**overrides),
106
+ Tools::GitDiff.new(**overrides),
107
+ Tools::GitLog.new(**overrides),
108
+ Tools::GitShow.new(**overrides),
109
+ Tools::GitBlame.new(**overrides),
110
+ Tools::GitGrep.new(**overrides),
111
+ Tools::GitBranch.new(**overrides),
112
+ Tools::JsonQuery.new(**overrides),
113
+ Tools::YamlQuery.new(**overrides),
114
+ Tools::TomlQuery.new(**overrides),
115
+ Tools::CsvRead.new(**overrides),
116
+ Tools::Calculator.new(**overrides),
117
+ Tools::DateTime.new(**overrides),
118
+ Tools::Diff.new(**overrides),
119
+ Tools::TodoWrite.new(**overrides),
120
+ Tools::ProcessOutput.new(**overrides),
121
+ Tools::ProcessList.new(**overrides),
122
+ Tools::ProcessKill.new(**overrides)
123
+ ]
124
+ end
125
+
126
+ # Tools whose effects are gated behind enable_exec_tools.
127
+ def exec_tools(**overrides)
128
+ [
129
+ Tools::WriteFile.new(**overrides),
130
+ Tools::EditFile.new(**overrides),
131
+ Tools::MultiEdit.new(**overrides),
132
+ Tools::CreateDirectory.new(**overrides),
133
+ Tools::MoveFile.new(**overrides),
134
+ Tools::DeleteFile.new(**overrides),
135
+ Tools::GitAdd.new(**overrides),
136
+ Tools::GitCommit.new(**overrides),
137
+ Tools::GitCheckout.new(**overrides),
138
+ Tools::ApplyPatch.new(**overrides),
139
+ Tools::CsvWrite.new(**overrides),
140
+ Tools::ReplaceInFiles.new(**overrides),
141
+ Tools::DownloadFile.new(**overrides),
142
+ Tools::RunTests.new(**overrides),
143
+ Tools::PythonTests.new(**overrides),
144
+ Tools::Lint.new(**overrides),
145
+ Tools::Bundle.new(**overrides),
146
+ Tools::BashTool.new(**overrides),
147
+ Tools::ProcessStart.new(**overrides),
148
+ Tools::RunRuby.new(**overrides),
149
+ Tools::RunPython.new(**overrides),
150
+ Tools::RunRust.new(**overrides)
151
+ ]
152
+ end
153
+
154
+ # Everything. Exec tools still honor the gate at call time, so handing the
155
+ # full set to a chat is safe by default.
156
+ def all_tools(**overrides)
157
+ safe_tools(**overrides) + exec_tools(**overrides)
158
+ end
159
+ end
160
+ end
161
+ end