tool_forge 0.0.1 → 0.2.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/README.md +317 -9
- data/Rakefile +3 -3
- data/examples/README.md +95 -0
- data/examples/docker_copy_tool.rb +82 -0
- data/examples/file_processor_tool.rb +148 -0
- data/lib/tool_forge/tool_definition.rb +274 -0
- data/lib/tool_forge/version.rb +2 -1
- data/lib/tool_forge.rb +50 -2
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b224a5666b3a1f3129f0f6c433d0da618be11d7e96401cdf632b268bb4667b3
|
4
|
+
data.tar.gz: e9e1b5d02fcbb000c8ef15b6e7c1707ac214b5b35d9346c711ab150507ee8209
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d50f61a50e87a6a3c5754b4d0f71910363651386cc47bb3634f5bc888f35214737449c10c411085fb95888f41ba9e0c96af7168be31a7f165ecb104a904627bc
|
7
|
+
data.tar.gz: 4d02123db7d4b56c83bdf53913f238c5e408f250edfdcbd2e963abcfd0049e2736762b2e4798ff7d9b6440b2242e1be39fcb5e9c3b640d29b66153e7bc3b0591
|
data/README.md
CHANGED
@@ -1,28 +1,336 @@
|
|
1
1
|
# ToolForge
|
2
2
|
|
3
|
-
|
3
|
+
ToolForge is a Ruby gem that provides a unified DSL for defining tools that can be converted to both [RubyLLM](https://github.com/ruby-llm/ruby-llm) and [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) formats. Write your tool once, use it anywhere.
|
4
4
|
|
5
|
-
|
5
|
+
## Features
|
6
6
|
|
7
|
-
|
7
|
+
- 🎯 **Unified DSL**: Define tools once, convert to multiple formats
|
8
|
+
- 🔧 **Helper Methods**: Support for both instance and class helper methods
|
9
|
+
- 📊 **Type Safety**: Parameter validation and type conversion
|
10
|
+
- 🚀 **Framework Agnostic**: Works with RubyLLM and MCP frameworks
|
11
|
+
- 📝 **Clean API**: Intuitive, Ruby-like syntax
|
8
12
|
|
9
|
-
|
13
|
+
## Installation
|
10
14
|
|
11
15
|
Install the gem and add to the application's Gemfile by executing:
|
12
16
|
|
13
17
|
```bash
|
14
|
-
bundle add
|
18
|
+
bundle add tool_forge
|
15
19
|
```
|
16
20
|
|
17
21
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
18
22
|
|
19
23
|
```bash
|
20
|
-
gem install
|
24
|
+
gem install tool_forge
|
25
|
+
```
|
26
|
+
|
27
|
+
## Quick Start
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'tool_forge'
|
31
|
+
|
32
|
+
# Define a tool
|
33
|
+
tool = ToolForge.define(:greet_user) do
|
34
|
+
description 'Greets a user with a personalized message'
|
35
|
+
|
36
|
+
param :name, type: :string, description: 'User name'
|
37
|
+
param :greeting, type: :string, description: 'Greeting style',
|
38
|
+
required: false, default: 'Hello'
|
39
|
+
|
40
|
+
execute do |name:, greeting:|
|
41
|
+
"#{greeting}, #{name}! Welcome to ToolForge!"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convert to RubyLLM format
|
46
|
+
ruby_llm_tool = tool.to_ruby_llm_tool
|
47
|
+
instance = ruby_llm_tool.new
|
48
|
+
result = instance.execute(name: 'Alice')
|
49
|
+
#=> "Hello, Alice! Welcome to ToolForge!"
|
50
|
+
|
51
|
+
# Convert to MCP format
|
52
|
+
mcp_tool = tool.to_mcp_tool
|
53
|
+
result = mcp_tool.call(server_context: nil, name: 'Bob')
|
54
|
+
#=> Returns MCP::Tool::Response object
|
55
|
+
```
|
56
|
+
|
57
|
+
## Detailed Usage
|
58
|
+
|
59
|
+
### Basic Tool Definition
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
tool = ToolForge.define(:file_reader) do
|
63
|
+
description 'Reads and processes files'
|
64
|
+
|
65
|
+
# Define parameters with types and validation
|
66
|
+
param :filename, type: :string, description: 'Path to file'
|
67
|
+
param :encoding, type: :string, required: false, default: 'utf-8'
|
68
|
+
param :max_lines, type: :integer, required: false
|
69
|
+
|
70
|
+
# Define execution logic
|
71
|
+
execute do |filename:, encoding:, max_lines:|
|
72
|
+
content = File.read(filename, encoding: encoding)
|
73
|
+
lines = content.lines
|
74
|
+
|
75
|
+
if max_lines
|
76
|
+
lines.first(max_lines).join
|
77
|
+
else
|
78
|
+
content
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
21
82
|
```
|
22
83
|
|
23
|
-
|
84
|
+
### Helper Methods
|
85
|
+
|
86
|
+
ToolForge supports two types of helper methods:
|
87
|
+
|
88
|
+
#### Instance Helper Methods
|
89
|
+
|
90
|
+
Use `helper` for methods that operate on instance data:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
tool = ToolForge.define(:text_processor) do
|
94
|
+
description 'Processes text with formatting'
|
95
|
+
|
96
|
+
param :text, type: :string
|
97
|
+
param :format, type: :string, default: 'uppercase'
|
98
|
+
|
99
|
+
# Instance helper method
|
100
|
+
helper(:format_text) do |text, format|
|
101
|
+
case format
|
102
|
+
when 'uppercase' then text.upcase
|
103
|
+
when 'lowercase' then text.downcase
|
104
|
+
when 'title' then text.split.map(&:capitalize).join(' ')
|
105
|
+
else text
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
helper(:add_prefix) do |text|
|
110
|
+
"PROCESSED: #{text}"
|
111
|
+
end
|
112
|
+
|
113
|
+
execute do |text:, format:|
|
114
|
+
formatted = format_text(text, format)
|
115
|
+
add_prefix(formatted)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
#### Class Helper Methods
|
121
|
+
|
122
|
+
Use `class_helper` for utility methods that don't depend on instance state:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
tool = ToolForge.define(:docker_copy) do
|
126
|
+
description 'Copies files to Docker containers'
|
127
|
+
|
128
|
+
param :container_id, type: :string
|
129
|
+
param :source_path, type: :string
|
130
|
+
param :dest_path, type: :string
|
131
|
+
|
132
|
+
# Class helper method - useful for utilities
|
133
|
+
class_helper(:add_to_tar) do |file_path, tar_path|
|
134
|
+
# Implementation for tar operations
|
135
|
+
"Added #{file_path} to tar archive as #{tar_path}"
|
136
|
+
end
|
137
|
+
|
138
|
+
class_helper(:validate_container) do |container_id|
|
139
|
+
# Validation logic
|
140
|
+
container_id.match?(/^[a-f0-9]{12}$/)
|
141
|
+
end
|
142
|
+
|
143
|
+
execute do |container_id:, source_path:, dest_path:|
|
144
|
+
return "Invalid container ID" unless self.class.validate_container(container_id)
|
145
|
+
|
146
|
+
tar_result = self.class.add_to_tar(source_path, dest_path)
|
147
|
+
"Copied #{source_path} to #{container_id}:#{dest_path} - #{tar_result}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
### Parameter Types
|
153
|
+
|
154
|
+
ToolForge supports various parameter types:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
tool = ToolForge.define(:complex_tool) do
|
158
|
+
param :name, type: :string # String parameter
|
159
|
+
param :count, type: :integer # Integer parameter
|
160
|
+
param :active, type: :boolean # Boolean parameter
|
161
|
+
param :rate, type: :number # Numeric parameter
|
162
|
+
param :tags, type: :array # Array parameter
|
163
|
+
param :metadata, type: :object # Object/Hash parameter
|
164
|
+
param :config, type: :string, required: false, default: 'default.json'
|
165
|
+
|
166
|
+
execute do |**params|
|
167
|
+
# Access all parameters
|
168
|
+
params.inspect
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
### Framework Integration
|
174
|
+
|
175
|
+
#### RubyLLM Integration
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
require 'ruby_llm'
|
179
|
+
require 'tool_forge'
|
180
|
+
|
181
|
+
tool = ToolForge.define(:my_tool) do
|
182
|
+
# ... tool definition
|
183
|
+
end
|
184
|
+
|
185
|
+
# Convert to RubyLLM format
|
186
|
+
ruby_llm_class = tool.to_ruby_llm_tool
|
187
|
+
|
188
|
+
# Use with RubyLLM
|
189
|
+
llm = RubyLLM::Client.new
|
190
|
+
llm.add_tool(ruby_llm_class)
|
191
|
+
```
|
192
|
+
|
193
|
+
#### MCP Integration
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
require 'mcp'
|
197
|
+
require 'tool_forge'
|
198
|
+
|
199
|
+
tool = ToolForge.define(:my_tool) do
|
200
|
+
# ... tool definition
|
201
|
+
end
|
202
|
+
|
203
|
+
# Convert to MCP format
|
204
|
+
mcp_class = tool.to_mcp_tool
|
205
|
+
|
206
|
+
# Use with MCP server
|
207
|
+
server = MCP::Server.new
|
208
|
+
server.add_tool(mcp_class)
|
209
|
+
```
|
210
|
+
|
211
|
+
## Advanced Features
|
212
|
+
|
213
|
+
### Complex Data Processing
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
tool = ToolForge.define(:data_analyzer) do
|
217
|
+
description 'Analyzes data files and generates reports'
|
218
|
+
|
219
|
+
param :files, type: :array, description: 'List of file paths'
|
220
|
+
param :output_format, type: :string, default: 'json'
|
221
|
+
|
222
|
+
helper(:read_file_data) do |file_path|
|
223
|
+
return { error: "File not found: #{file_path}" } unless File.exist?(file_path)
|
224
|
+
|
225
|
+
{
|
226
|
+
path: file_path,
|
227
|
+
size: File.size(file_path),
|
228
|
+
lines: File.readlines(file_path).count,
|
229
|
+
modified: File.mtime(file_path)
|
230
|
+
}
|
231
|
+
end
|
232
|
+
|
233
|
+
helper(:format_output) do |data, format|
|
234
|
+
case format
|
235
|
+
when 'json' then JSON.pretty_generate(data)
|
236
|
+
when 'yaml' then data.to_yaml
|
237
|
+
when 'csv' then data.map { |row| row.values.join(',') }.join("\n")
|
238
|
+
else data.inspect
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
execute do |files:, output_format:|
|
243
|
+
results = files.map { |file| read_file_data(file) }
|
244
|
+
|
245
|
+
summary = {
|
246
|
+
total_files: results.count,
|
247
|
+
total_size: results.sum { |r| r[:size] || 0 },
|
248
|
+
files: results
|
249
|
+
}
|
250
|
+
|
251
|
+
format_output(summary, output_format)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
### Error Handling
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
tool = ToolForge.define(:safe_processor) do
|
260
|
+
description 'Processes data with comprehensive error handling'
|
261
|
+
|
262
|
+
param :input, type: :string
|
263
|
+
param :operation, type: :string
|
264
|
+
|
265
|
+
helper(:validate_input) do |input|
|
266
|
+
raise ArgumentError, "Input cannot be empty" if input.nil? || input.empty?
|
267
|
+
raise ArgumentError, "Input too long" if input.length > 1000
|
268
|
+
true
|
269
|
+
end
|
270
|
+
|
271
|
+
execute do |input:, operation:|
|
272
|
+
begin
|
273
|
+
validate_input(input)
|
274
|
+
|
275
|
+
case operation
|
276
|
+
when 'reverse' then input.reverse
|
277
|
+
when 'upcase' then input.upcase
|
278
|
+
when 'analyze' then { length: input.length, words: input.split.count }
|
279
|
+
else
|
280
|
+
{ error: "Unknown operation: #{operation}" }
|
281
|
+
end
|
282
|
+
rescue => e
|
283
|
+
{ error: e.message }
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
## API Reference
|
290
|
+
|
291
|
+
### ToolForge.define(name, &block)
|
292
|
+
|
293
|
+
Creates a new tool definition.
|
294
|
+
|
295
|
+
- `name` (Symbol): The tool name
|
296
|
+
- `block`: Configuration block using the DSL
|
297
|
+
|
298
|
+
### DSL Methods
|
299
|
+
|
300
|
+
#### `description(text)`
|
301
|
+
Sets the tool description.
|
302
|
+
|
303
|
+
#### `param(name, options = {})`
|
304
|
+
Defines a parameter with options:
|
305
|
+
- `type`: Parameter type (`:string`, `:integer`, `:boolean`, `:number`, `:array`, `:object`)
|
306
|
+
- `description`: Parameter description
|
307
|
+
- `required`: Whether required (default: `true`)
|
308
|
+
- `default`: Default value for optional parameters
|
309
|
+
|
310
|
+
#### `helper(name, &block)`
|
311
|
+
Defines an instance helper method.
|
312
|
+
|
313
|
+
#### `class_helper(name, &block)`
|
314
|
+
Defines a class helper method.
|
315
|
+
|
316
|
+
#### `execute(&block)`
|
317
|
+
Defines the tool execution logic.
|
318
|
+
|
319
|
+
### Conversion Methods
|
320
|
+
|
321
|
+
#### `#to_ruby_llm_tool`
|
322
|
+
Converts to a RubyLLM::Tool class.
|
323
|
+
|
324
|
+
#### `#to_mcp_tool`
|
325
|
+
Converts to an MCP::Tool class.
|
326
|
+
|
327
|
+
## Examples
|
24
328
|
|
25
|
-
|
329
|
+
See the [examples directory](examples/) for more comprehensive examples including:
|
330
|
+
- File processing tools
|
331
|
+
- API integration tools
|
332
|
+
- Data transformation tools
|
333
|
+
- Docker management tools
|
26
334
|
|
27
335
|
## Development
|
28
336
|
|
@@ -32,7 +340,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
340
|
|
33
341
|
## Contributing
|
34
342
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
343
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/afstanton/tool_forge.
|
36
344
|
|
37
345
|
## License
|
38
346
|
|
data/Rakefile
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
5
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
7
7
|
|
8
|
-
require
|
8
|
+
require 'rubocop/rake_task'
|
9
9
|
|
10
10
|
RuboCop::RakeTask.new
|
11
11
|
|
data/examples/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# ToolForge Examples
|
2
|
+
|
3
|
+
This directory contains practical examples of ToolForge usage, demonstrating various features and patterns.
|
4
|
+
|
5
|
+
## Examples
|
6
|
+
|
7
|
+
### 1. Docker Copy Tool (`docker_copy_tool.rb`)
|
8
|
+
Demonstrates:
|
9
|
+
- **Class helper methods** for utility functions (`add_to_tar`, `validate_container_id`)
|
10
|
+
- **Instance helper methods** for result formatting
|
11
|
+
- **Parameter validation** and error handling
|
12
|
+
- **Complex return values** with structured data
|
13
|
+
|
14
|
+
**Key Features:**
|
15
|
+
- Container ID validation
|
16
|
+
- Tar archive operations simulation
|
17
|
+
- File existence checking
|
18
|
+
- Comprehensive error handling
|
19
|
+
|
20
|
+
### 2. File Processor Tool (`file_processor_tool.rb`)
|
21
|
+
Demonstrates:
|
22
|
+
- **Multiple instance helper methods** for different concerns
|
23
|
+
- **Array parameters** for multiple operations
|
24
|
+
- **Complex text processing** with various transformations
|
25
|
+
- **Multiple output formats** (JSON, YAML, text)
|
26
|
+
- **Preserve original data** option
|
27
|
+
|
28
|
+
**Key Features:**
|
29
|
+
- Text analysis (word count, line count, etc.)
|
30
|
+
- Text transformations (case changes, reversals, etc.)
|
31
|
+
- Flexible output formatting
|
32
|
+
- Operation chaining
|
33
|
+
|
34
|
+
## Running Examples
|
35
|
+
|
36
|
+
To run these examples:
|
37
|
+
|
38
|
+
```bash
|
39
|
+
# From the project root
|
40
|
+
ruby examples/docker_copy_tool.rb
|
41
|
+
ruby examples/file_processor_tool.rb
|
42
|
+
```
|
43
|
+
|
44
|
+
## Creating Your Own Tools
|
45
|
+
|
46
|
+
Use these examples as templates for your own tools:
|
47
|
+
|
48
|
+
1. **Start with a clear purpose** - what does your tool do?
|
49
|
+
2. **Define parameters** - what inputs does it need?
|
50
|
+
3. **Break down complex logic** into helper methods
|
51
|
+
4. **Choose the right helper type**:
|
52
|
+
- Use `helper` for instance methods that work with tool data
|
53
|
+
- Use `class_helper` for utility functions and static operations
|
54
|
+
5. **Handle errors gracefully** - return structured error information
|
55
|
+
6. **Consider output format** - how will users consume the results?
|
56
|
+
|
57
|
+
## Integration Examples
|
58
|
+
|
59
|
+
### With RubyLLM
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
require 'ruby_llm'
|
63
|
+
require_relative 'docker_copy_tool'
|
64
|
+
|
65
|
+
# Convert to RubyLLM format
|
66
|
+
ruby_llm_tool = docker_copy_tool.to_ruby_llm_tool
|
67
|
+
|
68
|
+
# Use with RubyLLM framework
|
69
|
+
llm = RubyLLM::Client.new
|
70
|
+
llm.add_tool(ruby_llm_tool)
|
71
|
+
```
|
72
|
+
|
73
|
+
### With MCP
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
require 'mcp'
|
77
|
+
require_relative 'file_processor_tool'
|
78
|
+
|
79
|
+
# Convert to MCP format
|
80
|
+
mcp_tool = file_processor_tool.to_mcp_tool
|
81
|
+
|
82
|
+
# Use with MCP server
|
83
|
+
server = MCP::Server.new
|
84
|
+
server.add_tool(mcp_tool)
|
85
|
+
```
|
86
|
+
|
87
|
+
## Best Practices Demonstrated
|
88
|
+
|
89
|
+
1. **Clear parameter documentation** - each parameter has a description
|
90
|
+
2. **Sensible defaults** - optional parameters have reasonable defaults
|
91
|
+
3. **Input validation** - check inputs before processing
|
92
|
+
4. **Error handling** - graceful failure with informative messages
|
93
|
+
5. **Structured outputs** - consistent, parseable return values
|
94
|
+
6. **Helper method organization** - logical separation of concerns
|
95
|
+
7. **Type safety** - appropriate parameter types for inputs
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example: Docker Copy Tool
|
4
|
+
# Demonstrates class helper methods for utility functions
|
5
|
+
|
6
|
+
require 'tool_forge'
|
7
|
+
|
8
|
+
docker_copy_tool = ToolForge.define(:docker_copy) do
|
9
|
+
description 'Copies files to Docker containers with tar archive support'
|
10
|
+
|
11
|
+
param :container_id, type: :string, description: 'Docker container ID'
|
12
|
+
param :source_path, type: :string, description: 'Local file path to copy'
|
13
|
+
param :dest_path, type: :string, description: 'Destination path in container'
|
14
|
+
param :create_archive, type: :boolean, required: false, default: true
|
15
|
+
|
16
|
+
# Class helper method for tar operations
|
17
|
+
class_helper(:add_to_tar) do |file_path, tar_path|
|
18
|
+
# In a real implementation, this would create a tar archive
|
19
|
+
{
|
20
|
+
operation: 'tar_add',
|
21
|
+
source: file_path,
|
22
|
+
target: tar_path,
|
23
|
+
timestamp: Time.now.iso8601,
|
24
|
+
success: true
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Class helper method for container validation
|
29
|
+
class_helper(:validate_container_id) do |container_id|
|
30
|
+
# Simple validation - real implementation would check with Docker
|
31
|
+
container_id.match?(/^[a-f0-9]{12,64}$/)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Instance helper method for formatting results
|
35
|
+
helper(:format_result) do |operation_result, container_id, dest_path|
|
36
|
+
if operation_result[:success]
|
37
|
+
"✅ Successfully copied to #{container_id}:#{dest_path} at #{operation_result[:timestamp]}"
|
38
|
+
else
|
39
|
+
"❌ Failed to copy: #{operation_result[:error]}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
execute do |container_id:, source_path:, dest_path:, create_archive:|
|
44
|
+
# Validate container ID using class helper
|
45
|
+
unless self.class.validate_container_id(container_id)
|
46
|
+
return { error: "Invalid container ID format: #{container_id}" }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if source file exists
|
50
|
+
return { error: "Source file not found: #{source_path}" } unless File.exist?(source_path)
|
51
|
+
|
52
|
+
begin
|
53
|
+
if create_archive
|
54
|
+
# Use class helper for tar operations
|
55
|
+
tar_result = self.class.add_to_tar(source_path, dest_path)
|
56
|
+
|
57
|
+
# Use instance helper for formatting
|
58
|
+
format_result(tar_result, container_id, dest_path)
|
59
|
+
else
|
60
|
+
# Direct copy simulation
|
61
|
+
{
|
62
|
+
message: "Direct copy to #{container_id}:#{dest_path}",
|
63
|
+
source: source_path,
|
64
|
+
destination: dest_path,
|
65
|
+
method: 'direct_copy',
|
66
|
+
timestamp: Time.now.iso8601
|
67
|
+
}
|
68
|
+
end
|
69
|
+
rescue StandardError => e
|
70
|
+
{ error: "Copy operation failed: #{e.message}" }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Example usage:
|
76
|
+
if __FILE__ == $PROGRAM_NAME
|
77
|
+
# This would normally require the actual RubyLLM or MCP frameworks
|
78
|
+
puts 'Docker Copy Tool Definition Created'
|
79
|
+
puts "Description: #{docker_copy_tool.description}"
|
80
|
+
puts "Parameters: #{docker_copy_tool.params.map { |p| p[:name] }.join(', ')}"
|
81
|
+
puts "Helper methods: #{docker_copy_tool.helper_methods.map { |type, methods| "#{type}: #{methods.keys}" }}"
|
82
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example: File Processor Tool
|
4
|
+
# Demonstrates instance helper methods and complex data processing
|
5
|
+
|
6
|
+
require 'tool_forge'
|
7
|
+
|
8
|
+
file_processor_tool = ToolForge.define(:file_processor) do
|
9
|
+
description 'Processes text files with various transformations and analysis'
|
10
|
+
|
11
|
+
param :file_path, type: :string, description: 'Path to the file to process'
|
12
|
+
param :operations, type: :array, description: 'List of operations to perform'
|
13
|
+
param :output_format, type: :string, required: false, default: 'json'
|
14
|
+
param :preserve_original, type: :boolean, required: false, default: true
|
15
|
+
|
16
|
+
# Instance helper methods for text processing
|
17
|
+
helper(:read_file_content) do |path|
|
18
|
+
return { error: "File not found: #{path}" } unless File.exist?(path)
|
19
|
+
|
20
|
+
{
|
21
|
+
content: File.read(path),
|
22
|
+
size: File.size(path),
|
23
|
+
lines: File.readlines(path).count,
|
24
|
+
encoding: File.read(path).encoding.name
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
helper(:analyze_text) do |content|
|
29
|
+
lines = content.lines
|
30
|
+
words = content.split(/\s+/)
|
31
|
+
|
32
|
+
{
|
33
|
+
character_count: content.length,
|
34
|
+
word_count: words.length,
|
35
|
+
line_count: lines.length,
|
36
|
+
average_line_length: lines.empty? ? 0 : (content.length.to_f / lines.length).round(2),
|
37
|
+
longest_word: words.max_by(&:length) || '',
|
38
|
+
unique_words: words.map(&:downcase).uniq.length
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
helper(:transform_text) do |content, operation|
|
43
|
+
case operation.downcase
|
44
|
+
when 'uppercase'
|
45
|
+
content.upcase
|
46
|
+
when 'lowercase'
|
47
|
+
content.downcase
|
48
|
+
when 'title_case'
|
49
|
+
content.split.map(&:capitalize).join(' ')
|
50
|
+
when 'reverse_lines'
|
51
|
+
content.lines.reverse.join
|
52
|
+
when 'reverse_words'
|
53
|
+
content.split.reverse.join(' ')
|
54
|
+
when 'remove_blank_lines'
|
55
|
+
content.lines.reject { |line| line.strip.empty? }.join
|
56
|
+
when 'number_lines'
|
57
|
+
content.lines.map.with_index(1) { |line, i| "#{i}. #{line}" }.join
|
58
|
+
else
|
59
|
+
content
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
helper(:format_output) do |data, format|
|
64
|
+
case format.downcase
|
65
|
+
when 'json'
|
66
|
+
JSON.pretty_generate(data)
|
67
|
+
when 'yaml'
|
68
|
+
begin
|
69
|
+
require 'yaml'
|
70
|
+
data.to_yaml
|
71
|
+
rescue LoadError
|
72
|
+
"YAML not available, falling back to JSON:\n#{JSON.pretty_generate(data)}"
|
73
|
+
end
|
74
|
+
when 'text'
|
75
|
+
if data.is_a?(Hash)
|
76
|
+
data.map { |k, v| "#{k}: #{v}" }.join("\n")
|
77
|
+
else
|
78
|
+
data.to_s
|
79
|
+
end
|
80
|
+
else
|
81
|
+
data.inspect
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
execute do |file_path:, operations:, output_format:, preserve_original:|
|
86
|
+
# Read the file
|
87
|
+
file_data = read_file_content(file_path)
|
88
|
+
return file_data if file_data[:error]
|
89
|
+
|
90
|
+
content = file_data[:content]
|
91
|
+
original_content = preserve_original ? content.dup : nil
|
92
|
+
|
93
|
+
# Process each operation
|
94
|
+
processed_content = content
|
95
|
+
operation_results = []
|
96
|
+
|
97
|
+
operations.each do |operation|
|
98
|
+
case operation.downcase
|
99
|
+
when 'analyze'
|
100
|
+
analysis = analyze_text(processed_content)
|
101
|
+
operation_results << { operation: operation, result: analysis }
|
102
|
+
else
|
103
|
+
# Text transformation
|
104
|
+
old_content = processed_content
|
105
|
+
processed_content = transform_text(processed_content, operation)
|
106
|
+
operation_results << {
|
107
|
+
operation: operation,
|
108
|
+
applied: old_content != processed_content,
|
109
|
+
preview: processed_content[0..100] + (processed_content.length > 100 ? '...' : '')
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Prepare final result
|
115
|
+
result = {
|
116
|
+
file_info: file_data.except(:content),
|
117
|
+
operations_applied: operation_results,
|
118
|
+
final_content: processed_content,
|
119
|
+
processing_summary: {
|
120
|
+
operations_count: operations.length,
|
121
|
+
content_changed: preserve_original ? (original_content != processed_content) : nil,
|
122
|
+
final_size: processed_content.length
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
result[:original_content] = original_content if preserve_original
|
127
|
+
|
128
|
+
# Format output according to preference
|
129
|
+
format_output(result, output_format)
|
130
|
+
rescue StandardError => e
|
131
|
+
format_output({ error: e.message, backtrace: e.backtrace.first(3) }, output_format)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Example usage:
|
136
|
+
if __FILE__ == $PROGRAM_NAME
|
137
|
+
puts 'File Processor Tool Definition Created'
|
138
|
+
puts "Description: #{file_processor_tool.description}"
|
139
|
+
puts "Parameters: #{file_processor_tool.params.map { |p| "#{p[:name]} (#{p[:type]})" }.join(', ')}"
|
140
|
+
|
141
|
+
# Example of what the tool can do:
|
142
|
+
puts "\nSupported operations:"
|
143
|
+
puts '- analyze: Provides text statistics'
|
144
|
+
puts '- uppercase, lowercase, title_case: Text case transformations'
|
145
|
+
puts '- reverse_lines, reverse_words: Content reversal'
|
146
|
+
puts '- remove_blank_lines: Cleanup operations'
|
147
|
+
puts '- number_lines: Add line numbers'
|
148
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ToolForge
|
4
|
+
# ToolDefinition is the core class for defining tools that can be converted
|
5
|
+
# to both RubyLLM and MCP tool formats. It provides a clean DSL for defining
|
6
|
+
# tool metadata, parameters, helper methods, and execution logic.
|
7
|
+
#
|
8
|
+
# @example Basic tool definition
|
9
|
+
# tool = ToolForge::ToolDefinition.new(:greet_user) do
|
10
|
+
# description 'Greets a user by name'
|
11
|
+
# param :name, type: :string, description: 'User name'
|
12
|
+
# execute { |name:| "Hello, #{name}!" }
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @example Tool with helper methods
|
16
|
+
# tool = ToolForge::ToolDefinition.new(:file_processor) do
|
17
|
+
# description 'Processes files with helper methods'
|
18
|
+
# param :file_path, type: :string
|
19
|
+
#
|
20
|
+
# # Instance helper method
|
21
|
+
# helper(:format_data) { |data| "FORMATTED: #{data}" }
|
22
|
+
#
|
23
|
+
# # Class helper method (useful for utilities like tar operations)
|
24
|
+
# class_helper(:add_to_tar) { |file, path| "Added #{file} to #{path}" }
|
25
|
+
#
|
26
|
+
# execute do |file_path:|
|
27
|
+
# data = File.read(file_path)
|
28
|
+
# formatted = format_data(data)
|
29
|
+
# tar_result = self.class.add_to_tar(file_path, '/archive')
|
30
|
+
# "#{formatted} - #{tar_result}"
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
class ToolDefinition
|
34
|
+
# @return [Symbol] the name of the tool
|
35
|
+
# @return [Array<Hash>] the parameters defined for the tool
|
36
|
+
# @return [Proc] the execution block for the tool
|
37
|
+
# @return [Hash] the helper methods organized by type (:instance and :class)
|
38
|
+
attr_reader :name, :params, :execute_block, :helper_methods
|
39
|
+
|
40
|
+
# Creates a new tool definition with the given name.
|
41
|
+
#
|
42
|
+
# @param name [Symbol] the name of the tool
|
43
|
+
# @yield [] optional block for configuring the tool using the DSL
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# tool = ToolDefinition.new(:my_tool) do
|
47
|
+
# description 'A sample tool'
|
48
|
+
# param :input, type: :string
|
49
|
+
# execute { |input:| "Processed: #{input}" }
|
50
|
+
# end
|
51
|
+
def initialize(name, &)
|
52
|
+
@name = name
|
53
|
+
@description = nil
|
54
|
+
@params = []
|
55
|
+
@execute_block = nil
|
56
|
+
@helper_methods = { instance: {}, class: {} }
|
57
|
+
|
58
|
+
instance_eval(&) if block_given?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sets or returns the tool description.
|
62
|
+
#
|
63
|
+
# @param text [String, nil] the description text to set, or nil to return current description
|
64
|
+
# @return [String, nil] the current description when called without arguments
|
65
|
+
#
|
66
|
+
# @example Setting description
|
67
|
+
# description 'This tool processes files'
|
68
|
+
#
|
69
|
+
# @example Getting description
|
70
|
+
# tool.description #=> 'This tool processes files'
|
71
|
+
def description(text = nil)
|
72
|
+
if text
|
73
|
+
@description = text
|
74
|
+
else
|
75
|
+
@description
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Defines a parameter for the tool.
|
80
|
+
#
|
81
|
+
# @param name [Symbol] the parameter name
|
82
|
+
# @param type [Symbol] the parameter type (:string, :integer, :boolean, etc.)
|
83
|
+
# @param description [String, nil] optional description of the parameter
|
84
|
+
# @param required [Boolean] whether the parameter is required (default: true)
|
85
|
+
# @param default [Object, nil] default value for optional parameters
|
86
|
+
#
|
87
|
+
# @example Required string parameter
|
88
|
+
# param :filename, type: :string, description: 'File to process'
|
89
|
+
#
|
90
|
+
# @example Optional parameter with default
|
91
|
+
# param :format, type: :string, description: 'Output format', required: false, default: 'json'
|
92
|
+
def param(name, type: :string, description: nil, required: true, default: nil)
|
93
|
+
@params << {
|
94
|
+
name: name,
|
95
|
+
type: type,
|
96
|
+
description: description,
|
97
|
+
required: required,
|
98
|
+
default: default
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Defines the execution logic for the tool.
|
103
|
+
#
|
104
|
+
# @yield [**args] block that receives tool parameters as keyword arguments
|
105
|
+
# @return [void]
|
106
|
+
#
|
107
|
+
# @example
|
108
|
+
# execute do |filename:, format:|
|
109
|
+
# data = File.read(filename)
|
110
|
+
# format == 'json' ? JSON.parse(data) : data
|
111
|
+
# end
|
112
|
+
def execute(&block)
|
113
|
+
@execute_block = block
|
114
|
+
end
|
115
|
+
|
116
|
+
# Defines an instance helper method that can be called within the execute block.
|
117
|
+
# Instance helper methods are available as regular method calls in the execution context.
|
118
|
+
#
|
119
|
+
# @param method_name [Symbol] the name of the helper method
|
120
|
+
# @yield [*args] block that defines the helper method logic
|
121
|
+
# @return [void]
|
122
|
+
#
|
123
|
+
# @example
|
124
|
+
# helper(:format_output) do |data|
|
125
|
+
# "FORMATTED: #{data.upcase}"
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# execute do |input:|
|
129
|
+
# format_output(input) # Called as instance method
|
130
|
+
# end
|
131
|
+
def helper(method_name, &block)
|
132
|
+
@helper_methods[:instance][method_name] = block
|
133
|
+
end
|
134
|
+
|
135
|
+
# Defines a class helper method that can be called within the execute block.
|
136
|
+
# Class helper methods are useful for utility functions that don't depend on instance state.
|
137
|
+
# They are accessed via self.class.method_name in the execution context.
|
138
|
+
#
|
139
|
+
# @param method_name [Symbol] the name of the class helper method
|
140
|
+
# @yield [*args] block that defines the helper method logic
|
141
|
+
# @return [void]
|
142
|
+
#
|
143
|
+
# @example
|
144
|
+
# class_helper(:add_to_tar) do |file_path, tar_path|
|
145
|
+
# # Implementation for adding files to tar archive
|
146
|
+
# "Added #{file_path} to tar as #{tar_path}"
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# execute do |file:|
|
150
|
+
# self.class.add_to_tar(file, '/archive/file.tar') # Called as class method
|
151
|
+
# end
|
152
|
+
def class_helper(method_name, &block)
|
153
|
+
@helper_methods[:class][method_name] = block
|
154
|
+
end
|
155
|
+
|
156
|
+
# Converts this tool definition to a RubyLLM::Tool class.
|
157
|
+
# The resulting class can be instantiated and used with the RubyLLM framework.
|
158
|
+
#
|
159
|
+
# Instance helper methods become instance methods on the generated class.
|
160
|
+
# Class helper methods become singleton methods on the generated class.
|
161
|
+
#
|
162
|
+
# @return [Class] a class that inherits from RubyLLM::Tool
|
163
|
+
# @raise [LoadError] if RubyLLM is not loaded
|
164
|
+
#
|
165
|
+
# @example
|
166
|
+
# tool_class = tool_definition.to_ruby_llm_tool
|
167
|
+
# instance = tool_class.new
|
168
|
+
# result = instance.execute(filename: 'data.txt')
|
169
|
+
def to_ruby_llm_tool
|
170
|
+
raise LoadError, 'RubyLLM is not loaded. Please require "ruby_llm" first.' unless defined?(RubyLLM::Tool)
|
171
|
+
raise LoadError, 'RubyLLM is not loaded. Please require "ruby_llm" first.' if RubyLLM::Tool.nil?
|
172
|
+
|
173
|
+
definition = self
|
174
|
+
|
175
|
+
Class.new(RubyLLM::Tool) do
|
176
|
+
description definition.description
|
177
|
+
|
178
|
+
definition.params.each do |param_def|
|
179
|
+
param param_def[:name], type: param_def[:type], desc: param_def[:description]
|
180
|
+
end
|
181
|
+
|
182
|
+
# Add instance helper methods
|
183
|
+
definition.helper_methods[:instance].each do |method_name, method_block|
|
184
|
+
define_method(method_name, &method_block)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Add class helper methods
|
188
|
+
definition.helper_methods[:class].each do |method_name, method_block|
|
189
|
+
define_singleton_method(method_name, &method_block)
|
190
|
+
end
|
191
|
+
|
192
|
+
define_method(:execute) do |**args|
|
193
|
+
# Execute the block in the context of this instance so helper methods are available
|
194
|
+
instance_exec(**args, &definition.execute_block)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Converts this tool definition to an MCP::Tool class.
|
200
|
+
# The resulting class can be used with the Model Context Protocol framework.
|
201
|
+
#
|
202
|
+
# Both instance and class helper methods are available in the execution context.
|
203
|
+
# Class helper methods are accessed via self.class.method_name.
|
204
|
+
#
|
205
|
+
# @return [Class] a class that inherits from MCP::Tool
|
206
|
+
# @raise [LoadError] if MCP SDK is not loaded
|
207
|
+
#
|
208
|
+
# @example
|
209
|
+
# tool_class = tool_definition.to_mcp_tool
|
210
|
+
# result = tool_class.call(server_context: nil, filename: 'data.txt')
|
211
|
+
def to_mcp_tool
|
212
|
+
raise LoadError, 'MCP SDK is not loaded. Please require "mcp" first.' unless defined?(MCP::Tool)
|
213
|
+
raise LoadError, 'MCP SDK is not loaded. Please require "mcp" first.' if MCP::Tool.nil?
|
214
|
+
|
215
|
+
definition = self
|
216
|
+
|
217
|
+
Class.new(MCP::Tool) do
|
218
|
+
description definition.description
|
219
|
+
|
220
|
+
# Build properties hash for input schema
|
221
|
+
properties = {}
|
222
|
+
required_params = []
|
223
|
+
|
224
|
+
definition.params.each do |param_def|
|
225
|
+
prop = {
|
226
|
+
type: param_def[:type].to_s
|
227
|
+
}
|
228
|
+
prop[:description] = param_def[:description] if param_def[:description]
|
229
|
+
|
230
|
+
properties[param_def[:name].to_s] = prop
|
231
|
+
required_params << param_def[:name].to_s if param_def[:required]
|
232
|
+
end
|
233
|
+
|
234
|
+
input_schema(
|
235
|
+
properties: properties,
|
236
|
+
required: required_params
|
237
|
+
)
|
238
|
+
|
239
|
+
# Create a helper object that contains all the helper methods
|
240
|
+
helper_class = Class.new do
|
241
|
+
definition.helper_methods[:instance].each do |method_name, method_block|
|
242
|
+
define_method(method_name, &method_block)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Class methods are defined as singleton methods on the class itself
|
246
|
+
definition.helper_methods[:class].each do |method_name, method_block|
|
247
|
+
define_singleton_method(method_name, &method_block)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
define_singleton_method(:call) do |server_context:, **args|
|
252
|
+
# Create an instance of the helper class to provide context for helper methods
|
253
|
+
helper_instance = helper_class.new
|
254
|
+
|
255
|
+
# Execute the block in the context of the helper instance so helper methods are available
|
256
|
+
# For class methods, they'll be available on the helper_class itself
|
257
|
+
result = helper_instance.instance_exec(**args, &definition.execute_block)
|
258
|
+
|
259
|
+
# Smart formatting for different return types
|
260
|
+
result_text = case result
|
261
|
+
when String
|
262
|
+
result
|
263
|
+
when Hash, Array
|
264
|
+
JSON.pretty_generate(result)
|
265
|
+
else
|
266
|
+
result.to_s
|
267
|
+
end
|
268
|
+
|
269
|
+
MCP::Tool::Response.new([{ type: 'text', text: result_text }])
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
data/lib/tool_forge/version.rb
CHANGED
data/lib/tool_forge.rb
CHANGED
@@ -1,8 +1,56 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'json'
|
4
|
+
require 'zeitwerk'
|
4
5
|
|
6
|
+
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
7
|
+
loader.setup # ready!
|
8
|
+
|
9
|
+
require_relative 'tool_forge/version'
|
10
|
+
|
11
|
+
# ToolForge provides a unified DSL for defining tools that can be converted
|
12
|
+
# to both RubyLLM and Model Context Protocol (MCP) formats.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# tool = ToolForge.define(:greet_user) do
|
16
|
+
# description 'Greets a user by name'
|
17
|
+
# param :name, type: :string, description: 'User name'
|
18
|
+
# execute { |name:| "Hello, #{name}!" }
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# # Convert to RubyLLM format
|
22
|
+
# ruby_llm_tool = tool.to_ruby_llm_tool
|
23
|
+
#
|
24
|
+
# # Convert to MCP format
|
25
|
+
# mcp_tool = tool.to_mcp_tool
|
5
26
|
module ToolForge
|
27
|
+
# Base error class for ToolForge-specific errors
|
6
28
|
class Error < StandardError; end
|
7
|
-
|
29
|
+
|
30
|
+
# Creates a new tool definition with the given name and configuration block.
|
31
|
+
#
|
32
|
+
# @param name [Symbol] the name of the tool
|
33
|
+
# @yield [] block for configuring the tool using the DSL
|
34
|
+
# @return [ToolDefinition] a new tool definition instance
|
35
|
+
#
|
36
|
+
# @example Define a simple tool
|
37
|
+
# tool = ToolForge.define(:calculator) do
|
38
|
+
# description 'Performs basic arithmetic'
|
39
|
+
# param :operation, type: :string, description: 'Operation to perform'
|
40
|
+
# param :a, type: :number, description: 'First number'
|
41
|
+
# param :b, type: :number, description: 'Second number'
|
42
|
+
#
|
43
|
+
# execute do |operation:, a:, b:|
|
44
|
+
# case operation
|
45
|
+
# when 'add' then a + b
|
46
|
+
# when 'subtract' then a - b
|
47
|
+
# when 'multiply' then a * b
|
48
|
+
# when 'divide' then b != 0 ? a / b : 'Cannot divide by zero'
|
49
|
+
# else 'Unknown operation'
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
def self.define(name, &)
|
54
|
+
ToolDefinition.new(name, &)
|
55
|
+
end
|
8
56
|
end
|
metadata
CHANGED
@@ -1,14 +1,28 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tool_forge
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron F Stanton
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: zeitwerk
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
12
26
|
description: A Ruby gem for building AI tools for large language models using a simple
|
13
27
|
domain-specific language.
|
14
28
|
email:
|
@@ -20,7 +34,11 @@ files:
|
|
20
34
|
- LICENSE.txt
|
21
35
|
- README.md
|
22
36
|
- Rakefile
|
37
|
+
- examples/README.md
|
38
|
+
- examples/docker_copy_tool.rb
|
39
|
+
- examples/file_processor_tool.rb
|
23
40
|
- lib/tool_forge.rb
|
41
|
+
- lib/tool_forge/tool_definition.rb
|
24
42
|
- lib/tool_forge/version.rb
|
25
43
|
- sig/tool_forge.rbs
|
26
44
|
homepage: https://github.com/afstanton/tool_forge
|
@@ -30,6 +48,7 @@ metadata:
|
|
30
48
|
allowed_push_host: https://rubygems.org
|
31
49
|
homepage_uri: https://github.com/afstanton/tool_forge
|
32
50
|
source_code_uri: https://github.com/afstanton/tool_forge
|
51
|
+
rubygems_mfa_required: 'true'
|
33
52
|
rdoc_options: []
|
34
53
|
require_paths:
|
35
54
|
- lib
|