shared_tools 0.4.4 → 0.7.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 +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +211 -7
- data/lib/shared_tools/apply_patch_tool.rb +4 -0
- data/lib/shared_tools/bash_tool.rb +4 -0
- data/lib/shared_tools/bundle_tool.rb +4 -0
- data/lib/shared_tools/command_guard.rb +35 -0
- data/lib/shared_tools/csv_read_tool.rb +4 -0
- data/lib/shared_tools/csv_write_tool.rb +4 -0
- data/lib/shared_tools/data_path.rb +52 -0
- data/lib/shared_tools/diff_tool.rb +4 -0
- data/lib/shared_tools/download_file_tool.rb +4 -0
- data/lib/shared_tools/gem_tool.rb +4 -0
- data/lib/shared_tools/git_tool.rb +5 -0
- data/lib/shared_tools/glob_tool.rb +4 -0
- data/lib/shared_tools/http_request_tool.rb +4 -0
- data/lib/shared_tools/json_query_tool.rb +4 -0
- data/lib/shared_tools/lint_tool.rb +4 -0
- data/lib/shared_tools/mcp/brave_search_client.rb +18 -16
- data/lib/shared_tools/mcp/chart_client.rb +18 -15
- data/lib/shared_tools/mcp/client_support.rb +103 -0
- data/lib/shared_tools/mcp/github_client.rb +23 -19
- data/lib/shared_tools/mcp/hugging_face_client.rb +22 -22
- data/lib/shared_tools/mcp/memory_client.rb +20 -15
- data/lib/shared_tools/mcp/notion_client.rb +21 -19
- data/lib/shared_tools/mcp/playwright_client.rb +19 -15
- data/lib/shared_tools/mcp/sequential_thinking_client.rb +17 -14
- data/lib/shared_tools/mcp/slack_client.rb +30 -25
- data/lib/shared_tools/mcp/tavily_client.rb +21 -21
- data/lib/shared_tools/mcp.rb +32 -41
- data/lib/shared_tools/multi_edit_tool.rb +4 -0
- data/lib/shared_tools/parse_ruby_tool.rb +4 -0
- data/lib/shared_tools/process_kill_tool.rb +4 -0
- data/lib/shared_tools/process_list_tool.rb +4 -0
- data/lib/shared_tools/process_output_tool.rb +4 -0
- data/lib/shared_tools/process_registry.rb +224 -0
- data/lib/shared_tools/process_runner.rb +69 -0
- data/lib/shared_tools/process_start_tool.rb +4 -0
- data/lib/shared_tools/python_tests_tool.rb +4 -0
- data/lib/shared_tools/replace_in_files_tool.rb +4 -0
- data/lib/shared_tools/ruby_outline.rb +207 -0
- data/lib/shared_tools/run_tests_tool.rb +4 -0
- data/lib/shared_tools/text_diff.rb +80 -0
- data/lib/shared_tools/todo_write_tool.rb +4 -0
- data/lib/shared_tools/toml_parser.rb +409 -0
- data/lib/shared_tools/toml_query_tool.rb +4 -0
- data/lib/shared_tools/tools/apply_patch_tool.rb +122 -0
- data/lib/shared_tools/tools/bash_tool.rb +102 -0
- data/lib/shared_tools/tools/browser/click_tool.rb +2 -2
- data/lib/shared_tools/tools/browser/inspect_tool.rb +2 -2
- data/lib/shared_tools/tools/browser/page_inspect_tool.rb +2 -2
- data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +2 -2
- data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +2 -2
- data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +2 -2
- data/lib/shared_tools/tools/browser/visit_tool.rb +2 -2
- data/lib/shared_tools/tools/browser_tool.rb +2 -2
- data/lib/shared_tools/tools/bundle_tool.rb +96 -0
- data/lib/shared_tools/tools/calculator_tool.rb +2 -2
- data/lib/shared_tools/tools/clipboard_tool.rb +2 -2
- data/lib/shared_tools/tools/composite_analysis_tool.rb +2 -2
- data/lib/shared_tools/tools/computer_tool.rb +2 -2
- data/lib/shared_tools/tools/cron_tool.rb +2 -2
- data/lib/shared_tools/tools/csv_read_tool.rb +82 -0
- data/lib/shared_tools/tools/csv_write_tool.rb +67 -0
- data/lib/shared_tools/tools/current_date_time_tool.rb +3 -3
- data/lib/shared_tools/tools/data_science_kit.rb +2 -2
- data/lib/shared_tools/tools/database_query_tool.rb +2 -2
- data/lib/shared_tools/tools/database_tool.rb +2 -2
- data/lib/shared_tools/tools/diff_tool.rb +48 -0
- data/lib/shared_tools/tools/disk/directory_create_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/directory_delete_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/directory_list_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/directory_move_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/file_create_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/file_delete_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/file_move_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/file_read_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/file_replace_tool.rb +2 -2
- data/lib/shared_tools/tools/disk/file_write_tool.rb +2 -2
- data/lib/shared_tools/tools/disk_tool.rb +2 -2
- data/lib/shared_tools/tools/dns_tool.rb +2 -2
- data/lib/shared_tools/tools/doc/docx_reader_tool.rb +2 -2
- data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +2 -2
- data/lib/shared_tools/tools/doc/spreadsheet_reader_tool.rb +2 -2
- data/lib/shared_tools/tools/doc/text_reader_tool.rb +2 -2
- data/lib/shared_tools/tools/doc_tool.rb +2 -2
- data/lib/shared_tools/tools/docker/compose_run_tool.rb +2 -2
- data/lib/shared_tools/tools/download_file_tool.rb +87 -0
- data/lib/shared_tools/tools/enabler.rb +2 -2
- data/lib/shared_tools/tools/error_handling_tool.rb +2 -2
- data/lib/shared_tools/tools/eval/python_eval_tool.rb +2 -2
- data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +2 -2
- data/lib/shared_tools/tools/eval/shell_eval_tool.rb +2 -2
- data/lib/shared_tools/tools/eval_tool.rb +2 -2
- data/lib/shared_tools/tools/gem_tool.rb +157 -0
- data/lib/shared_tools/tools/git/add_tool.rb +73 -0
- data/lib/shared_tools/tools/git/blame_tool.rb +77 -0
- data/lib/shared_tools/tools/git/branch_tool.rb +55 -0
- data/lib/shared_tools/tools/git/checkout_tool.rb +69 -0
- data/lib/shared_tools/tools/git/commit_tool.rb +73 -0
- data/lib/shared_tools/tools/git/diff_tool.rb +69 -0
- data/lib/shared_tools/tools/git/grep_tool.rb +82 -0
- data/lib/shared_tools/tools/git/helpers.rb +70 -0
- data/lib/shared_tools/tools/git/log_tool.rb +71 -0
- data/lib/shared_tools/tools/git/show_tool.rb +66 -0
- data/lib/shared_tools/tools/git/status_tool.rb +46 -0
- data/lib/shared_tools/tools/git.rb +18 -0
- data/lib/shared_tools/tools/git_tool.rb +148 -0
- data/lib/shared_tools/tools/glob_tool.rb +75 -0
- data/lib/shared_tools/tools/http_helpers.rb +119 -0
- data/lib/shared_tools/tools/http_request_tool.rb +91 -0
- data/lib/shared_tools/tools/json_query_tool.rb +81 -0
- data/lib/shared_tools/tools/lint_tool.rb +93 -0
- data/lib/shared_tools/tools/multi_edit_tool.rb +134 -0
- data/lib/shared_tools/tools/notification_tool.rb +2 -2
- data/lib/shared_tools/tools/parse_ruby_tool.rb +127 -0
- data/lib/shared_tools/tools/process_kill_tool.rb +55 -0
- data/lib/shared_tools/tools/process_list_tool.rb +41 -0
- data/lib/shared_tools/tools/process_output_tool.rb +69 -0
- data/lib/shared_tools/tools/process_start_tool.rb +0 -0
- data/lib/shared_tools/tools/python_tests_tool.rb +105 -0
- data/lib/shared_tools/tools/replace_in_files_tool.rb +166 -0
- data/lib/shared_tools/tools/run_tests_tool.rb +108 -0
- data/lib/shared_tools/tools/search_codebase_tool.rb +182 -51
- data/lib/shared_tools/tools/secure_tool_template.rb +2 -2
- data/lib/shared_tools/tools/system_info_tool.rb +2 -2
- data/lib/shared_tools/tools/todo_write_tool.rb +81 -0
- data/lib/shared_tools/tools/toml_query_tool.rb +81 -0
- data/lib/shared_tools/tools/toolchain_helpers.rb +64 -0
- data/lib/shared_tools/tools/tree_tool.rb +100 -0
- data/lib/shared_tools/tools/weather_tool.rb +2 -2
- data/lib/shared_tools/tools/web_fetch_tool.rb +85 -0
- data/lib/shared_tools/tools/workflow_manager_tool.rb +2 -2
- data/lib/shared_tools/tools/yaml_query_tool.rb +86 -0
- data/lib/shared_tools/tree_tool.rb +4 -0
- data/lib/shared_tools/url_guard.rb +106 -0
- data/lib/shared_tools/utilities.rb +2 -2
- data/lib/shared_tools/version.rb +1 -1
- data/lib/shared_tools/web_fetch_tool.rb +4 -0
- data/lib/shared_tools/yaml_query_tool.rb +4 -0
- data/lib/shared_tools.rb +1 -1
- metadata +532 -409
- data/lib/shared_tools/mcp/streamable_http_patch.rb +0 -42
- data/lib/shared_tools/tools/version.rb +0 -7
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../shared_tools"
|
|
4
|
+
require_relative "../process_runner"
|
|
5
|
+
require_relative "../command_guard"
|
|
6
|
+
|
|
7
|
+
module SharedTools
|
|
8
|
+
module Tools
|
|
9
|
+
# Runs ONE allowlisted executable with arguments.
|
|
10
|
+
#
|
|
11
|
+
# Deliberately NOT a shell: there are no pipes, redirects, globs,
|
|
12
|
+
# quoting, or variable expansion. The program goes in `command`; each
|
|
13
|
+
# argument is a separate element of `args` and is passed verbatim as
|
|
14
|
+
# argv. This is the safe primitive that the OS-command-injection class of
|
|
15
|
+
# bug cannot touch, because no shell ever parses the input.
|
|
16
|
+
#
|
|
17
|
+
# Requires user authorization (see SharedTools.execute?) AND the
|
|
18
|
+
# executable being on allowed_commands (empty by default — nothing is
|
|
19
|
+
# allowed until the caller configures it).
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# tool = SharedTools::Tools::BashTool.new(allowed_commands: ["ls", "cat"])
|
|
23
|
+
# tool.execute(command: "ls", args: ["-la"])
|
|
24
|
+
class BashTool < ::RubyLLM::Tool
|
|
25
|
+
DEFAULT_TIMEOUT = 30
|
|
26
|
+
|
|
27
|
+
def self.tool_name = 'bash'
|
|
28
|
+
|
|
29
|
+
description "Run a single ALLOWLISTED executable with arguments. " \
|
|
30
|
+
"No shell is involved: no pipes, redirects, globs, or variable expansion. " \
|
|
31
|
+
"Put the program name in `command` and each argument as its own element of `args`."
|
|
32
|
+
|
|
33
|
+
parameters do
|
|
34
|
+
string :command, description: "Executable name (must be on the allowlist). No path, no shell characters."
|
|
35
|
+
array :args, of: :string, description: "Arguments passed verbatim to the program, one per element. Optional.", required: false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param allowed_commands [Array<String>] executables permitted to run. Empty by default (nothing allowed).
|
|
39
|
+
# @param logger [Logger] optional logger
|
|
40
|
+
def initialize(allowed_commands: [], logger: nil)
|
|
41
|
+
@allowed_commands = allowed_commands
|
|
42
|
+
@logger = logger || RubyLLM.logger
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param command [String]
|
|
46
|
+
# @param args [Array<String>, nil]
|
|
47
|
+
#
|
|
48
|
+
# @return [String, Hash]
|
|
49
|
+
def execute(command:, args: nil)
|
|
50
|
+
@logger.info("#{self.class.name}#execute command=#{command.inspect} args=#{args.inspect}")
|
|
51
|
+
|
|
52
|
+
exe = SharedTools::CommandGuard.new(@allowed_commands).check!(command)
|
|
53
|
+
argv = sanitize_args(args)
|
|
54
|
+
|
|
55
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: ([exe] + argv).join(" "))
|
|
56
|
+
unless allowed
|
|
57
|
+
@logger.warn("User declined to run #{([exe] + argv).join(' ')}")
|
|
58
|
+
return { error: "User declined to run the command" }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
out, err, status = SharedTools::ProcessRunner.capture(
|
|
62
|
+
[exe, *argv],
|
|
63
|
+
env: clean_env,
|
|
64
|
+
timeout: DEFAULT_TIMEOUT,
|
|
65
|
+
unsetenv_others: true
|
|
66
|
+
)
|
|
67
|
+
format_result(exe, argv, out, err, status)
|
|
68
|
+
rescue SharedTools::CommandGuard::Blocked => e
|
|
69
|
+
@logger.error("#{self.class.name} command denied: #{e.message}")
|
|
70
|
+
{ error: e.message }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def sanitize_args(args)
|
|
76
|
+
nul = 0.chr
|
|
77
|
+
Array(args).map do |arg|
|
|
78
|
+
str = arg.to_s
|
|
79
|
+
raise SharedTools::CommandGuard::Blocked, "argument contains a NUL byte" if str.include?(nul)
|
|
80
|
+
|
|
81
|
+
str
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def clean_env
|
|
86
|
+
{}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def format_result(exe, argv, out, err, status)
|
|
90
|
+
body = +"argv: #{([exe] + argv).inspect}\n"
|
|
91
|
+
body << if status == :timeout
|
|
92
|
+
"result: timed out after #{DEFAULT_TIMEOUT}s (killed)\n"
|
|
93
|
+
else
|
|
94
|
+
"exit: #{status.exitstatus}\n"
|
|
95
|
+
end
|
|
96
|
+
body << "\n--- stdout ---\n#{out}" unless out.empty?
|
|
97
|
+
body << "\n--- stderr ---\n#{err}" unless err.empty?
|
|
98
|
+
body
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -11,11 +11,11 @@ module SharedTools
|
|
|
11
11
|
# tool.execute(selector: "some text")
|
|
12
12
|
# tool.execute(selector: "//div[@role='button']")
|
|
13
13
|
class ClickTool < ::RubyLLM::Tool
|
|
14
|
-
def self.
|
|
14
|
+
def self.tool_name = 'browser_click'
|
|
15
15
|
|
|
16
16
|
description "A browser automation tool for clicking any clickable element."
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
parameters do
|
|
19
19
|
string :selector, description: <<~TEXT.strip
|
|
20
20
|
A CSS selector to locate or interact with an element on the page:
|
|
21
21
|
|
|
@@ -11,13 +11,13 @@ module SharedTools
|
|
|
11
11
|
module Browser
|
|
12
12
|
# A browser automation tool for finding UI elements by their text content.
|
|
13
13
|
class InspectTool < ::RubyLLM::Tool
|
|
14
|
-
def self.
|
|
14
|
+
def self.tool_name = 'browser_inspect'
|
|
15
15
|
|
|
16
16
|
include InspectUtils
|
|
17
17
|
|
|
18
18
|
description "A browser automation tool for finding UI elements by their text content."
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :text_content, description: "Search for elements containing this text"
|
|
22
22
|
string :selector, description: "Optional CSS selector to further filter results", required: false
|
|
23
23
|
integer :context_size, description: "Number of parent elements to include for context", required: false
|
|
@@ -11,13 +11,13 @@ module SharedTools
|
|
|
11
11
|
module Browser
|
|
12
12
|
# A browser automation tool for viewing the full HTML of the page.
|
|
13
13
|
class PageInspectTool < ::RubyLLM::Tool
|
|
14
|
-
def self.
|
|
14
|
+
def self.tool_name = 'browser_page_inspect'
|
|
15
15
|
|
|
16
16
|
include InspectUtils
|
|
17
17
|
|
|
18
18
|
description "A browser automation tool for viewing the full HTML of the current page."
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
boolean :summarize, description: "If true, returns a summary instead of full HTML", required: false
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -7,11 +7,11 @@ module SharedTools
|
|
|
7
7
|
# Saves the screenshot to a file and returns the path — avoids injecting
|
|
8
8
|
# large base64 blobs into the conversation context.
|
|
9
9
|
class PageScreenshotTool < ::RubyLLM::Tool
|
|
10
|
-
def self.
|
|
10
|
+
def self.tool_name = 'browser_page_screenshot'
|
|
11
11
|
|
|
12
12
|
description "Take a screenshot of the current browser page and save it to a file."
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
parameters do
|
|
15
15
|
string :path, required: false,
|
|
16
16
|
description: "File path to save the screenshot (e.g. 'screenshot.png'). " \
|
|
17
17
|
"Defaults to a timestamped name in the current directory."
|
|
@@ -11,13 +11,13 @@ module SharedTools
|
|
|
11
11
|
module Browser
|
|
12
12
|
# A browser automation tool for inspecting elements using CSS selectors.
|
|
13
13
|
class SelectorInspectTool < ::RubyLLM::Tool
|
|
14
|
-
def self.
|
|
14
|
+
def self.tool_name = 'browser_selector_inspect'
|
|
15
15
|
|
|
16
16
|
include InspectUtils
|
|
17
17
|
|
|
18
18
|
description "A browser automation tool for finding and inspecting elements by CSS selector."
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :selector, description: "CSS selector to target specific elements"
|
|
22
22
|
integer :context_size, description: "Number of parent elements to include for context", required: false
|
|
23
23
|
end
|
|
@@ -8,11 +8,11 @@ module SharedTools
|
|
|
8
8
|
# tool = SharedTools::Tools::Browser::TextFieldSetTool.new(browser:)
|
|
9
9
|
# tool.execute(selector: "...", text: "...")
|
|
10
10
|
class TextFieldAreaSetTool < ::RubyLLM::Tool
|
|
11
|
-
def self.
|
|
11
|
+
def self.tool_name = 'browser_text_field_set'
|
|
12
12
|
|
|
13
13
|
description "A browser automation tool for clicking a specific link."
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
parameters do
|
|
16
16
|
string :selector, description: "The ID / name of the text field / area to interact with."
|
|
17
17
|
string :text, description: "The text to set."
|
|
18
18
|
end
|
|
@@ -8,11 +8,11 @@ module SharedTools
|
|
|
8
8
|
# tool = SharedTools::Tools::Browser::VisitTool.new(driver:)
|
|
9
9
|
# tool.execute(to: "https://news.ycombinator.com")
|
|
10
10
|
class VisitTool < ::RubyLLM::Tool
|
|
11
|
-
def self.
|
|
11
|
+
def self.tool_name = 'browser_visit'
|
|
12
12
|
|
|
13
13
|
description "A browser automation tool for navigating to a specific URL."
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
parameters do
|
|
16
16
|
string :url, description: "A URL (e.g. https://news.ycombinator.com)."
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -6,7 +6,7 @@ module SharedTools
|
|
|
6
6
|
module Tools
|
|
7
7
|
# A tool for controller a browser.
|
|
8
8
|
class BrowserTool < ::RubyLLM::Tool
|
|
9
|
-
def self.
|
|
9
|
+
def self.tool_name = 'browser'
|
|
10
10
|
module Action
|
|
11
11
|
VISIT = "visit"
|
|
12
12
|
PAGE_INSPECT = "page_inspect"
|
|
@@ -86,7 +86,7 @@ module SharedTools
|
|
|
86
86
|
{"action": "#{Action::SCREENSHOT}"}
|
|
87
87
|
TEXT
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
parameters do
|
|
90
90
|
string :action, description: <<~TEXT.strip
|
|
91
91
|
The browser action to perform. Options:
|
|
92
92
|
* `#{Action::VISIT}`: Navigate to a website
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../shared_tools"
|
|
4
|
+
require_relative "toolchain_helpers"
|
|
5
|
+
|
|
6
|
+
module SharedTools
|
|
7
|
+
module Tools
|
|
8
|
+
# Runs Bundler operations in a project. The mutating sibling of the
|
|
9
|
+
# read-only GemTool. install/update/add reach the network and change the
|
|
10
|
+
# project's gems, so this requires user authorization (see
|
|
11
|
+
# SharedTools.execute?).
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# tool = SharedTools::Tools::BundleTool.new(root: "./my-project")
|
|
15
|
+
# tool.execute(action: "install")
|
|
16
|
+
# tool.execute(action: "add", gem: "rack")
|
|
17
|
+
class BundleTool < ::RubyLLM::Tool
|
|
18
|
+
include ToolchainHelpers
|
|
19
|
+
|
|
20
|
+
ACTIONS = %w[install update outdated check lock add].freeze
|
|
21
|
+
NAME_RE = /\A[A-Za-z0-9_.-]+\z/
|
|
22
|
+
DEFAULT_TIMEOUT = 180
|
|
23
|
+
|
|
24
|
+
def self.tool_name = 'bundle'
|
|
25
|
+
|
|
26
|
+
description "Run Bundler in a project. Actions: install, update, outdated, check, lock, add. " \
|
|
27
|
+
"For 'add' provide a gem name; for 'update' a gem name is optional (omit to update " \
|
|
28
|
+
"all). Requires a Gemfile."
|
|
29
|
+
|
|
30
|
+
parameters do
|
|
31
|
+
string :action, description: "One of: install, update, outdated, check, lock, add."
|
|
32
|
+
string :gem, description: "Gem name — required for 'add', optional for 'update'.", required: false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param root [String] optional, defaults to the current directory
|
|
36
|
+
# @param logger [Logger] optional logger
|
|
37
|
+
def initialize(root: nil, logger: nil)
|
|
38
|
+
@root = root || Dir.pwd
|
|
39
|
+
@logger = logger || RubyLLM.logger
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param action [String]
|
|
43
|
+
# @param gem [String, nil]
|
|
44
|
+
#
|
|
45
|
+
# @return [String, Hash]
|
|
46
|
+
def execute(action:, gem: nil)
|
|
47
|
+
@logger.info("#{self.class.name}#execute action=#{action} gem=#{gem.inspect}")
|
|
48
|
+
|
|
49
|
+
act = action.to_s.strip.downcase
|
|
50
|
+
return { error: "unknown action: #{act} (use #{ACTIONS.join(', ')})" } unless ACTIONS.include?(act)
|
|
51
|
+
return { error: "a Gemfile is required (none at #{@root})" } unless gemfile?
|
|
52
|
+
|
|
53
|
+
args = build_args(act, gem)
|
|
54
|
+
return args if args.is_a?(Hash) # validation error
|
|
55
|
+
|
|
56
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: args.join(" "))
|
|
57
|
+
unless allowed
|
|
58
|
+
@logger.warn("User declined to run #{args.join(' ')}")
|
|
59
|
+
return { error: "User declined to run #{args.join(' ')}" }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
out, err, status = run_in_project(args, use_bundle: false, timeout: DEFAULT_TIMEOUT)
|
|
63
|
+
toolchain_output(out, err, status,
|
|
64
|
+
pass_label: "bundle #{act}: ok",
|
|
65
|
+
fail_label: "bundle #{act}: failed",
|
|
66
|
+
timeout: DEFAULT_TIMEOUT)
|
|
67
|
+
rescue CommandMissing
|
|
68
|
+
@logger.error("#{self.class.name}: bundler is not available")
|
|
69
|
+
{ error: "bundler is not available (gem install bundler)" }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def build_args(act, gem_name)
|
|
75
|
+
case act
|
|
76
|
+
when "add"
|
|
77
|
+
name = gem_name.to_s.strip
|
|
78
|
+
return { error: "'add' requires a valid gem name" } unless name.match?(NAME_RE)
|
|
79
|
+
|
|
80
|
+
["bundle", "add", name]
|
|
81
|
+
when "update"
|
|
82
|
+
name = gem_name.to_s.strip
|
|
83
|
+
if name.empty?
|
|
84
|
+
["bundle", "update"]
|
|
85
|
+
else
|
|
86
|
+
return { error: "invalid gem name: #{name.inspect}" } unless name.match?(NAME_RE)
|
|
87
|
+
|
|
88
|
+
["bundle", "update", name]
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
["bundle", act]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -11,7 +11,7 @@ end
|
|
|
11
11
|
module SharedTools
|
|
12
12
|
module Tools
|
|
13
13
|
class CalculatorTool < RubyLLM::Tool
|
|
14
|
-
def self.
|
|
14
|
+
def self.tool_name = 'calculator'
|
|
15
15
|
|
|
16
16
|
def available?
|
|
17
17
|
DENTAKU_AVAILABLE
|
|
@@ -44,7 +44,7 @@ module SharedTools
|
|
|
44
44
|
puts "Result: #{result[:result]}" # 3.3333
|
|
45
45
|
DESCRIPTION
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
parameters do
|
|
48
48
|
string :expression, description: <<~DESC.strip
|
|
49
49
|
Mathematical expression to evaluate using standard arithmetic operators and parentheses.
|
|
50
50
|
Supported operations include: addition (+), subtraction (-), multiplication (*),
|
|
@@ -13,7 +13,7 @@ module SharedTools
|
|
|
13
13
|
# tool.execute(action: 'read')
|
|
14
14
|
# tool.execute(action: 'clear')
|
|
15
15
|
class ClipboardTool < ::RubyLLM::Tool
|
|
16
|
-
def self.
|
|
16
|
+
def self.tool_name = 'clipboard'
|
|
17
17
|
|
|
18
18
|
description <<~DESC
|
|
19
19
|
Read from, write to, and clear the system clipboard.
|
|
@@ -25,7 +25,7 @@ module SharedTools
|
|
|
25
25
|
- 'clear' — Empty the clipboard
|
|
26
26
|
DESC
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
parameters do
|
|
29
29
|
string :action, description: "Action to perform: 'read', 'write', or 'clear'"
|
|
30
30
|
string :text, required: false, description: "Text to write. Required only for the 'write' action."
|
|
31
31
|
end
|
|
@@ -5,7 +5,7 @@ require 'json'
|
|
|
5
5
|
module SharedTools
|
|
6
6
|
module Tools
|
|
7
7
|
class CompositeAnalysisTool < RubyLLM::Tool
|
|
8
|
-
def self.
|
|
8
|
+
def self.tool_name = "composite_analysis"
|
|
9
9
|
|
|
10
10
|
description <<~'DESCRIPTION'
|
|
11
11
|
Perform comprehensive multi-stage data analysis by orchestrating multiple specialized analysis steps
|
|
@@ -17,7 +17,7 @@ module SharedTools
|
|
|
17
17
|
from initial data loading through final insights. Handles CSV, JSON, and text data formats.
|
|
18
18
|
DESCRIPTION
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :data_source, description: <<~DESC.strip, required: false
|
|
22
22
|
Primary data source to analyze. Can be either a local file path or a web URL.
|
|
23
23
|
For files: Use relative or absolute paths to CSV, JSON, XML, or text files.
|
|
@@ -11,7 +11,7 @@ module SharedTools
|
|
|
11
11
|
# computer.execute(action: 'mouse_position')
|
|
12
12
|
# computer.execute(action: 'type', text: 'Hello')
|
|
13
13
|
class ComputerTool < ::RubyLLM::Tool
|
|
14
|
-
def self.
|
|
14
|
+
def self.tool_name = 'computer'
|
|
15
15
|
description "A tool for interacting with a computer."
|
|
16
16
|
|
|
17
17
|
module Action
|
|
@@ -70,7 +70,7 @@ module SharedTools
|
|
|
70
70
|
ScrollDirection::RIGHT,
|
|
71
71
|
].freeze
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
parameters do
|
|
74
74
|
string :action, description: <<~TEXT.strip
|
|
75
75
|
Options:
|
|
76
76
|
* `#{Action::KEY}`: Press a single key / combination of keys on the keyboard:
|
|
@@ -14,7 +14,7 @@ module SharedTools
|
|
|
14
14
|
# tool.execute(action: 'next_times', expression: '0 * * * *', count: 5)
|
|
15
15
|
# tool.execute(action: 'generate', description: 'every day at 9am')
|
|
16
16
|
class CronTool < ::RubyLLM::Tool
|
|
17
|
-
def self.
|
|
17
|
+
def self.tool_name = 'cron'
|
|
18
18
|
|
|
19
19
|
description <<~DESC
|
|
20
20
|
Parse, validate, explain, and generate cron expressions (standard 5-field format).
|
|
@@ -33,7 +33,7 @@ module SharedTools
|
|
|
33
33
|
'every weekday', 'first day of every month at midnight'
|
|
34
34
|
DESC
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
parameters do
|
|
37
37
|
string :action, description: "Action: 'parse', 'validate', 'next_times', 'generate'"
|
|
38
38
|
string :expression, required: false, description: "5-field cron expression. Required for parse, validate, next_times."
|
|
39
39
|
integer :count, required: false, description: "Number of next execution times. Default: 5."
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require_relative '../../shared_tools'
|
|
5
|
+
|
|
6
|
+
module SharedTools
|
|
7
|
+
module Tools
|
|
8
|
+
# Reads a CSV file and returns its rows in a compact, readable form.
|
|
9
|
+
# Read-only.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# tool = SharedTools::Tools::CsvReadTool.new
|
|
13
|
+
# tool.execute(path: "./sales.csv")
|
|
14
|
+
# tool.execute(path: "./sales.csv", headers: false, limit: 10)
|
|
15
|
+
class CsvReadTool < ::RubyLLM::Tool
|
|
16
|
+
MAX_BYTES = 10 * 1024 * 1024
|
|
17
|
+
DEFAULT_LIMIT = 100
|
|
18
|
+
|
|
19
|
+
def self.tool_name = 'csv_read'
|
|
20
|
+
|
|
21
|
+
description "Read a CSV file and return its rows. By default the first row is treated as a " \
|
|
22
|
+
"header. Use limit to cap the number of data rows returned."
|
|
23
|
+
|
|
24
|
+
parameters do
|
|
25
|
+
string :path, description: "CSV file to read."
|
|
26
|
+
boolean :headers, description: "Treat the first row as column headers. Default true.", required: false
|
|
27
|
+
integer :limit, description: "Maximum number of data rows to return (default #{DEFAULT_LIMIT}).", required: false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param logger [Logger] optional logger
|
|
31
|
+
def initialize(logger: nil)
|
|
32
|
+
@logger = logger || RubyLLM.logger
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param path [String]
|
|
36
|
+
# @param headers [Boolean]
|
|
37
|
+
# @param limit [Integer]
|
|
38
|
+
#
|
|
39
|
+
# @return [String, Hash]
|
|
40
|
+
def execute(path:, headers: true, limit: DEFAULT_LIMIT)
|
|
41
|
+
@logger.info("#{self.class.name}#execute path=#{path.inspect} headers=#{headers} limit=#{limit}")
|
|
42
|
+
|
|
43
|
+
return { error: "file not found: #{path}" } unless File.exist?(path)
|
|
44
|
+
return { error: "not a file: #{path}" } unless File.file?(path)
|
|
45
|
+
return { error: "file too large (> #{MAX_BYTES} bytes)" } if File.size(path) > MAX_BYTES
|
|
46
|
+
|
|
47
|
+
rows = CSV.read(path)
|
|
48
|
+
return "empty CSV" if rows.empty?
|
|
49
|
+
|
|
50
|
+
max = limit.to_i
|
|
51
|
+
max = DEFAULT_LIMIT if max <= 0
|
|
52
|
+
render(rows, headers, max)
|
|
53
|
+
rescue CSV::MalformedCSVError => e
|
|
54
|
+
@logger.error("#{self.class.name} malformed CSV: #{e.message}")
|
|
55
|
+
{ error: "malformed CSV: #{e.message}" }
|
|
56
|
+
rescue => e
|
|
57
|
+
@logger.error("#{self.class.name} failed: #{e.message}")
|
|
58
|
+
{ error: e.message }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def render(rows, headers, max)
|
|
64
|
+
header = headers ? rows.first : nil
|
|
65
|
+
data = headers ? rows[1..] : rows
|
|
66
|
+
shown = data.first(max)
|
|
67
|
+
|
|
68
|
+
lines = []
|
|
69
|
+
lines << "columns: #{header.join(' | ')}" if header
|
|
70
|
+
lines << "#{data.size} row#{data.size == 1 ? '' : 's'}#{data.size > max ? " (showing #{max})" : ''}"
|
|
71
|
+
shown.each_with_index do |row, i|
|
|
72
|
+
lines << if header
|
|
73
|
+
"#{i + 1}. " + header.zip(row).map { |k, v| "#{k}=#{v}" }.join(", ")
|
|
74
|
+
else
|
|
75
|
+
"#{i + 1}. " + row.join(" | ")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
lines.join("\n")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require_relative '../../shared_tools'
|
|
6
|
+
|
|
7
|
+
module SharedTools
|
|
8
|
+
module Tools
|
|
9
|
+
# Writes rows to a CSV file. Optional header row. Missing parent
|
|
10
|
+
# directories are created. Overwrites an existing file. Mutating —
|
|
11
|
+
# requires user authorization (see SharedTools.execute?).
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# tool = SharedTools::Tools::CsvWriteTool.new
|
|
15
|
+
# tool.execute(path: "./out.csv", headers: ["name", "age"], rows: [["Alice", 30]])
|
|
16
|
+
class CsvWriteTool < ::RubyLLM::Tool
|
|
17
|
+
def self.tool_name = 'csv_write'
|
|
18
|
+
|
|
19
|
+
description "Write rows to a CSV file. Provide rows as an array of arrays (each inner array " \
|
|
20
|
+
"is a row of cell values, written as strings), with an optional headers array " \
|
|
21
|
+
"written as the first line. Overwrites an existing file."
|
|
22
|
+
|
|
23
|
+
parameters do
|
|
24
|
+
string :path, description: "Destination CSV path."
|
|
25
|
+
array :rows, description: "Array of rows; each row is an array of cell values (as strings)." do
|
|
26
|
+
array of: :string
|
|
27
|
+
end
|
|
28
|
+
array :headers, of: :string, description: "Optional column header row, written first.", required: false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param logger [Logger] optional logger
|
|
32
|
+
def initialize(logger: nil)
|
|
33
|
+
@logger = logger || RubyLLM.logger
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param path [String]
|
|
37
|
+
# @param rows [Array<Array>]
|
|
38
|
+
# @param headers [Array<String>, nil]
|
|
39
|
+
#
|
|
40
|
+
# @return [String, Hash]
|
|
41
|
+
def execute(path:, rows:, headers: nil)
|
|
42
|
+
@logger.info("#{self.class.name}#execute path=#{path.inspect} rows=#{Array(rows).size}")
|
|
43
|
+
|
|
44
|
+
return { error: "rows must be an array of arrays" } unless rows.is_a?(Array)
|
|
45
|
+
return { error: "path is a directory: #{path}" } if File.directory?(path)
|
|
46
|
+
|
|
47
|
+
all = []
|
|
48
|
+
all << Array(headers) if headers && !Array(headers).empty?
|
|
49
|
+
rows.each { |row| all << Array(row) }
|
|
50
|
+
|
|
51
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: "Write #{rows.size} row(s) to #{path}")
|
|
52
|
+
unless allowed
|
|
53
|
+
@logger.warn("User declined to write CSV to #{path}")
|
|
54
|
+
return { error: "User declined to write the CSV file" }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
58
|
+
CSV.open(path, "w") { |csv| all.each { |row| csv << row } }
|
|
59
|
+
|
|
60
|
+
"Wrote #{rows.size} row#{rows.size == 1 ? '' : 's'} to #{path}"
|
|
61
|
+
rescue => e
|
|
62
|
+
@logger.error("#{self.class.name} failed: #{e.message}")
|
|
63
|
+
{ error: e.message }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -12,7 +12,7 @@ module SharedTools
|
|
|
12
12
|
# tool.execute # full output
|
|
13
13
|
# tool.execute(format: 'date') # date fields only
|
|
14
14
|
class CurrentDateTimeTool < ::RubyLLM::Tool
|
|
15
|
-
def self.
|
|
15
|
+
def self.tool_name = 'current_date_time'
|
|
16
16
|
|
|
17
17
|
description <<~DESC
|
|
18
18
|
Returns the current date, time, timezone, and calendar metadata from the system clock.
|
|
@@ -24,7 +24,7 @@ module SharedTools
|
|
|
24
24
|
- 'iso8601' — iso8601, iso8601_utc, unix_timestamp
|
|
25
25
|
DESC
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
parameters do
|
|
28
28
|
string :format, required: false, description: <<~DESC.strip
|
|
29
29
|
Output format. Options: 'full' (default), 'date', 'time', 'iso8601'.
|
|
30
30
|
DESC
|
|
@@ -41,7 +41,7 @@ module SharedTools
|
|
|
41
41
|
@logger.info("CurrentDateTimeTool#execute format=#{format}")
|
|
42
42
|
|
|
43
43
|
now = Time.now
|
|
44
|
-
utc = now.utc
|
|
44
|
+
utc = now.getutc # getutc returns a copy; Time#utc would convert now in place
|
|
45
45
|
|
|
46
46
|
date_info = {
|
|
47
47
|
year: now.year,
|
|
@@ -5,7 +5,7 @@ require 'json'
|
|
|
5
5
|
module SharedTools
|
|
6
6
|
module Tools
|
|
7
7
|
class DataScienceKit < RubyLLM::Tool
|
|
8
|
-
def self.
|
|
8
|
+
def self.tool_name = "data_science_kit"
|
|
9
9
|
|
|
10
10
|
description <<~'DESCRIPTION'
|
|
11
11
|
Comprehensive data science and analytics toolkit for performing statistical analysis,
|
|
@@ -17,7 +17,7 @@ module SharedTools
|
|
|
17
17
|
recommendations and statistical significance testing where applicable.
|
|
18
18
|
DESCRIPTION
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
parameters do
|
|
21
21
|
string :analysis_type, description: <<~DESC.strip, required: true
|
|
22
22
|
Type of data science analysis to perform:
|
|
23
23
|
- 'statistical_summary': Descriptive statistics, distributions, outlier detection
|
|
@@ -11,7 +11,7 @@ end
|
|
|
11
11
|
module SharedTools
|
|
12
12
|
module Tools
|
|
13
13
|
class DatabaseQueryTool < RubyLLM::Tool
|
|
14
|
-
def self.
|
|
14
|
+
def self.tool_name = 'database_query'
|
|
15
15
|
|
|
16
16
|
def available?
|
|
17
17
|
SEQUEL_AVAILABLE
|
|
@@ -41,7 +41,7 @@ module SharedTools
|
|
|
41
41
|
puts "Found #{result[:row_count]} users"
|
|
42
42
|
DESCRIPTION
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
parameters do
|
|
45
45
|
string :query, description: <<~DESC.strip
|
|
46
46
|
SQL SELECT query to execute against the database. Only SELECT statements are permitted
|
|
47
47
|
for security reasons - INSERT, UPDATE, DELETE, and DDL statements will be rejected.
|
|
@@ -10,7 +10,7 @@ module SharedTools
|
|
|
10
10
|
# tool = SharedTools::Tools::DatabaseTool.new(driver:)
|
|
11
11
|
# tool.execute(statements: ["SELECT * FROM people"])
|
|
12
12
|
class DatabaseTool < ::RubyLLM::Tool
|
|
13
|
-
def self.
|
|
13
|
+
def self.tool_name = 'database'
|
|
14
14
|
description <<~TEXT
|
|
15
15
|
Executes SQL commands (INSERT / UPDATE / SELECT / etc) on a database.
|
|
16
16
|
|
|
@@ -57,7 +57,7 @@ module SharedTools
|
|
|
57
57
|
]
|
|
58
58
|
TEXT
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
parameters do
|
|
61
61
|
array :statements, of: :string, description: "A list of SQL statements to run sequentially (e.g. ['SELECT * FROM users', 'INSERT INTO ...'])"
|
|
62
62
|
end
|
|
63
63
|
|