llm_gateway 0.1.4 → 0.1.5
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 +13 -2
- data/README.md +2 -2
- data/Rakefile +45 -7
- data/lib/llm_gateway/tool.rb +1 -1
- data/lib/llm_gateway/version.rb +1 -1
- data/sample/claude_code_clone/agent.rb +65 -0
- data/sample/claude_code_clone/claude_code_clone.rb +40 -0
- data/sample/claude_code_clone/prompt.rb +79 -0
- data/sample/{directory_bot → claude_code_clone}/run.rb +3 -5
- data/sample/claude_code_clone/tools/bash_tool.rb +54 -0
- data/sample/claude_code_clone/tools/edit_tool.rb +61 -0
- data/sample/claude_code_clone/tools/grep_tool.rb +113 -0
- data/sample/claude_code_clone/tools/read_tool.rb +61 -0
- data/sample/claude_code_clone/tools/todowrite_tool.rb +98 -0
- metadata +11 -6
- data/sample/directory_bot/file_search_bot.rb +0 -72
- data/sample/directory_bot/file_search_prompt.rb +0 -56
- data/sample/directory_bot/file_search_tool.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e5e5bb04da32e9a1af4ad9dea6e8bf5af8785fc58d890fcb903739e89bc1a50
|
4
|
+
data.tar.gz: c23364222e72a72eaf2754bf775cff9709004db2007ab210c6440a0a2cb99cdc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f028198b6ed363a858dc0d409943d117d7c6c538ba81f21797a3dde55de4ada0fcf5e4814776873b5e35db0e3c3f87b938019cc9330fedcdae2469a746db1c4
|
7
|
+
data.tar.gz: df939b1f9dea8204e3ae87d1050056ea79014e29917475d4131dce7162dfec28f225f39bb5419c3327bf01f3a32d23634e612983d67c85fc581140c03da50697
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [
|
3
|
+
## [v0.1.5](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.1.5) (2025-08-05)
|
4
4
|
|
5
|
-
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.
|
5
|
+
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.4...v0.1.5)
|
6
|
+
|
7
|
+
**Merged pull requests:**
|
8
|
+
|
9
|
+
- burn: login from tool base class [\#11](https://github.com/Hyper-Unearthing/llm_gateway/pull/11) ([billybonks](https://github.com/billybonks))
|
10
|
+
- improve sample [\#10](https://github.com/Hyper-Unearthing/llm_gateway/pull/10) ([billybonks](https://github.com/billybonks))
|
11
|
+
- ci: mark latest change log as a version [\#9](https://github.com/Hyper-Unearthing/llm_gateway/pull/9) ([billybonks](https://github.com/billybonks))
|
12
|
+
- ci: improve rake release task, so i get burnt less [\#8](https://github.com/Hyper-Unearthing/llm_gateway/pull/8) ([billybonks](https://github.com/billybonks))
|
13
|
+
|
14
|
+
## [v0.1.4](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.1.4) (2025-08-04)
|
15
|
+
|
16
|
+
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.3...v0.1.4)
|
6
17
|
|
7
18
|
**Merged pull requests:**
|
8
19
|
|
data/README.md
CHANGED
@@ -45,7 +45,7 @@ result = LlmGateway::Client.chat(
|
|
45
45
|
|
46
46
|
### Sample Application
|
47
47
|
|
48
|
-
See the [file search bot example](sample/
|
48
|
+
See the [file search bot example](sample/claude_code_clone/) for a complete working application that demonstrates:
|
49
49
|
- Creating reusable Prompt and Tool classes
|
50
50
|
- Handling conversation transcripts with tool execution
|
51
51
|
- Building an interactive terminal interface
|
@@ -53,7 +53,7 @@ See the [file search bot example](sample/directory_bot/) for a complete working
|
|
53
53
|
To run the sample:
|
54
54
|
|
55
55
|
```bash
|
56
|
-
cd sample/
|
56
|
+
cd sample/claude_code_clone
|
57
57
|
ruby run.rb
|
58
58
|
```
|
59
59
|
|
data/Rakefile
CHANGED
@@ -14,12 +14,29 @@ begin
|
|
14
14
|
|
15
15
|
desc "Release with changelog"
|
16
16
|
task :gem_release do
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
# Safety checks: ensure we're on main and up-to-date
|
18
|
+
current_branch = `git branch --show-current`.strip
|
19
|
+
unless current_branch == "main"
|
20
|
+
puts "Error: You must be on the main branch to release. Current branch: #{current_branch}"
|
21
|
+
exit 1
|
22
|
+
end
|
23
|
+
|
24
|
+
# Check if branch is up-to-date with remote
|
25
|
+
sh "git fetch origin"
|
26
|
+
local_commit = `git rev-parse HEAD`.strip
|
27
|
+
remote_commit = `git rev-parse origin/main`.strip
|
28
|
+
unless local_commit == remote_commit
|
29
|
+
puts "Error: Your main branch is not in sync with origin/main. Please pull the latest changes."
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check for uncommitted changes
|
34
|
+
unless `git status --porcelain`.strip.empty?
|
35
|
+
puts "Error: You have uncommitted changes. Please commit or stash them before releasing."
|
36
|
+
exit 1
|
37
|
+
end
|
21
38
|
|
22
|
-
# Ask for version bump type
|
39
|
+
# Ask for version bump type first
|
23
40
|
print "What type of version bump? (major/minor/patch): "
|
24
41
|
version_type = $stdin.gets.chomp.downcase
|
25
42
|
|
@@ -28,8 +45,29 @@ begin
|
|
28
45
|
exit 1
|
29
46
|
end
|
30
47
|
|
31
|
-
#
|
32
|
-
sh "gem bump --version #{version_type} --
|
48
|
+
# Bump version without committing yet to get new version
|
49
|
+
sh "gem bump --version #{version_type} --no-commit"
|
50
|
+
|
51
|
+
# Get the new version
|
52
|
+
new_version = `ruby -e "puts Gem::Specification.load('llm_gateway.gemspec').version"`.strip
|
53
|
+
|
54
|
+
# Generate changelog with proper version
|
55
|
+
sh "bundle exec github_changelog_generator " \
|
56
|
+
"-u Hyper-Unearthing -p llm_gateway --future-release v#{new_version}"
|
57
|
+
|
58
|
+
# Bundle to update Gemfile.lock
|
59
|
+
sh "bundle"
|
60
|
+
|
61
|
+
# Add all changes and commit in one go
|
62
|
+
sh "git add ."
|
63
|
+
sh "git commit -m 'Bump llm_gateway to $(ruby -e \"puts Gem::Specification.load('llm_gateway.gemspec').version\")'"
|
64
|
+
|
65
|
+
# Tag and push
|
66
|
+
sh "git tag v$(ruby -e \"puts Gem::Specification.load('llm_gateway.gemspec').version\")"
|
67
|
+
sh "git push origin main --tags"
|
68
|
+
|
69
|
+
# Release the gem
|
70
|
+
sh "gem push $(gem build llm_gateway.gemspec | grep 'File:' | awk '{print $2}')"
|
33
71
|
end
|
34
72
|
rescue LoadError
|
35
73
|
# gem-release not available in this environment
|
data/lib/llm_gateway/tool.rb
CHANGED
data/lib/llm_gateway/version.rb
CHANGED
@@ -0,0 +1,65 @@
|
|
1
|
+
class Agent
|
2
|
+
def initialize(prompt_class, model, api_key)
|
3
|
+
@prompt_class = prompt_class
|
4
|
+
@model = model
|
5
|
+
@api_key = api_key
|
6
|
+
@transcript = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(user_input, &block)
|
10
|
+
@transcript << { role: 'user', content: [ { type: 'text', text: user_input } ] }
|
11
|
+
|
12
|
+
begin
|
13
|
+
prompt = @prompt_class.new(@model, @transcript, @api_key)
|
14
|
+
result = prompt.post
|
15
|
+
process_response(result[:choices][0][:content], &block)
|
16
|
+
rescue => e
|
17
|
+
yield({ type: 'error', message: e.message }) if block_given?
|
18
|
+
raise e
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def process_response(response, &block)
|
25
|
+
@transcript << { role: 'assistant', content: response }
|
26
|
+
|
27
|
+
response.each do |message|
|
28
|
+
yield(message) if block_given?
|
29
|
+
|
30
|
+
if message[:type] == 'text'
|
31
|
+
# Text response processed
|
32
|
+
elsif message[:type] == 'tool_use'
|
33
|
+
result = handle_tool_use(message)
|
34
|
+
|
35
|
+
tool_result = {
|
36
|
+
type: 'tool_result',
|
37
|
+
tool_use_id: message[:id],
|
38
|
+
content: result
|
39
|
+
}
|
40
|
+
@transcript << { role: 'user', content: [ tool_result ] }
|
41
|
+
|
42
|
+
yield(tool_result) if block_given?
|
43
|
+
|
44
|
+
follow_up_prompt = @prompt_class.new(@model, @transcript, @api_key)
|
45
|
+
follow_up = follow_up_prompt.post
|
46
|
+
|
47
|
+
process_response(follow_up[:choices][0][:content], &block) if follow_up[:choices][0][:content]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
response
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle_tool_use(message)
|
55
|
+
tool_class = @prompt_class.find_tool(message[:name])
|
56
|
+
if tool_class
|
57
|
+
tool = tool_class.new
|
58
|
+
tool.execute(message[:input])
|
59
|
+
else
|
60
|
+
"Unknown tool: #{message[:name]}"
|
61
|
+
end
|
62
|
+
rescue StandardError => e
|
63
|
+
"Error executing tool: #{e.message}"
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'prompt'
|
2
|
+
require_relative 'agent'
|
3
|
+
require 'debug'
|
4
|
+
|
5
|
+
# Bash File Search Assistant using LlmGateway architecture
|
6
|
+
|
7
|
+
class ClaudeCloneClone
|
8
|
+
def initialize(model, api_key)
|
9
|
+
@agent = Agent.new(Prompt, model, api_key)
|
10
|
+
end
|
11
|
+
|
12
|
+
def query(input)
|
13
|
+
begin
|
14
|
+
@agent.run(input) do |message|
|
15
|
+
case message[:type]
|
16
|
+
when 'text'
|
17
|
+
puts "\n\e[32m•\e[0m #{message[:text]}"
|
18
|
+
when 'tool_use'
|
19
|
+
puts "\n\e[33m•\e[0m \e[36m#{message[:name]}\e[0m"
|
20
|
+
if message[:input] && !message[:input].empty?
|
21
|
+
puts " \e[90m#{message[:input]}\e[0m"
|
22
|
+
end
|
23
|
+
when 'tool_result'
|
24
|
+
if message[:content] && !message[:content].empty?
|
25
|
+
content_preview = message[:content].to_s.split("\n").first(3).join("\n")
|
26
|
+
if content_preview.length > 100
|
27
|
+
content_preview = content_preview[0..97] + "..."
|
28
|
+
end
|
29
|
+
puts " \e[90m#{content_preview}\e[0m"
|
30
|
+
end
|
31
|
+
when 'error'
|
32
|
+
puts "\n\e[31m•\e[0m \e[91mError: #{message[:message]}\e[0m"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
rescue => e
|
36
|
+
puts "\n\e[31m•\e[0m \e[91mError: #{e.message}\e[0m"
|
37
|
+
puts "\e[90m #{e.backtrace.first}\e[0m" if e.backtrace&.first
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'tools/edit_tool'
|
2
|
+
require_relative 'tools/read_tool'
|
3
|
+
require_relative 'tools/todowrite_tool'
|
4
|
+
require_relative 'tools/bash_tool'
|
5
|
+
require_relative 'tools/grep_tool'
|
6
|
+
|
7
|
+
class Prompt < LlmGateway::Prompt
|
8
|
+
def initialize(model, transcript, api_key)
|
9
|
+
super(model)
|
10
|
+
@transcript = transcript
|
11
|
+
@api_key = api_key
|
12
|
+
end
|
13
|
+
|
14
|
+
def prompt
|
15
|
+
@transcript
|
16
|
+
end
|
17
|
+
|
18
|
+
def system_prompt
|
19
|
+
<<~SYSTEM
|
20
|
+
You are Claude Code Clone, an interactive CLI tool that assists with software engineering tasks.
|
21
|
+
|
22
|
+
# Core Capabilities
|
23
|
+
|
24
|
+
I provide assistance with:
|
25
|
+
- Code analysis and debugging
|
26
|
+
- Feature implementation
|
27
|
+
- File editing and creation
|
28
|
+
- Running tests and builds
|
29
|
+
- Git operations
|
30
|
+
- Web browsing and research
|
31
|
+
- Task planning and management
|
32
|
+
|
33
|
+
## Available Tools
|
34
|
+
|
35
|
+
You have access to these specialized tools:
|
36
|
+
- `Edit` - Modify existing files by replacing specific text strings
|
37
|
+
- `Read` - Read file contents with optional pagination
|
38
|
+
- `TodoWrite` - Create and manage structured task lists
|
39
|
+
- `Bash` - Execute shell commands with timeout support
|
40
|
+
- `Grep` - Search for patterns in files using regex
|
41
|
+
|
42
|
+
## Core Instructions
|
43
|
+
|
44
|
+
I am designed to:
|
45
|
+
- Be concise and direct (minimize output tokens)
|
46
|
+
- Follow existing code conventions and patterns
|
47
|
+
- Use defensive security practices only
|
48
|
+
- Plan tasks with the TodoWrite tool for complex work
|
49
|
+
- Run linting/typechecking after making changes
|
50
|
+
- Never commit unless explicitly asked
|
51
|
+
|
52
|
+
## Process
|
53
|
+
|
54
|
+
1. **Understand the Request**: Parse what the user needs accomplished
|
55
|
+
2. **Plan if Complex**: Use TodoWrite for multi-step tasks
|
56
|
+
3. **Execute Tools**: Use appropriate tools to complete the work
|
57
|
+
4. **Validate**: Run tests/linting when applicable
|
58
|
+
5. **Report**: Provide concise status updates
|
59
|
+
|
60
|
+
Always use the available tools to perform actions rather than just suggesting commands.
|
61
|
+
|
62
|
+
Before starting any task, build a todo list of what you need to do, ensuring each item is actionable and prioritized. Then, execute the tasks one by one, using the TodoWrite tool to track progress and completion.
|
63
|
+
|
64
|
+
After completing each task, update the TodoWrite list to reflect the status and any necessary follow-up actions.
|
65
|
+
SYSTEM
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.tools
|
69
|
+
[ EditTool, ReadTool, TodoWriteTool, BashTool, GrepTool ]
|
70
|
+
end
|
71
|
+
|
72
|
+
def tools
|
73
|
+
self.class.tools.map(&:definition)
|
74
|
+
end
|
75
|
+
|
76
|
+
def post
|
77
|
+
LlmGateway::Client.chat(model, prompt, tools: tools, system: system_prompt, api_key: @api_key)
|
78
|
+
end
|
79
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'tty-prompt'
|
2
2
|
require_relative '../../lib/llm_gateway'
|
3
|
-
require_relative '
|
3
|
+
require_relative 'claude_code_clone.rb'
|
4
4
|
|
5
5
|
# Terminal Runner for FileSearchBot
|
6
6
|
class FileSearchTerminalRunner
|
@@ -9,17 +9,15 @@ class FileSearchTerminalRunner
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def start
|
12
|
-
puts "File Search Assistant - I can help you find files and search through directories."
|
13
12
|
puts "First, let's configure your LLM settings:\n\n"
|
14
13
|
|
15
14
|
model, api_key = setup_configuration
|
16
|
-
bot =
|
15
|
+
bot = ClaudeCloneClone.new(model, api_key)
|
17
16
|
|
18
|
-
puts "\nGreat! Now you can start searching."
|
19
17
|
puts "Type 'quit' or 'exit' to stop.\n\n"
|
20
18
|
|
21
19
|
loop do
|
22
|
-
user_input = @prompt.ask("What
|
20
|
+
user_input = @prompt.ask("What can i do for you?")
|
23
21
|
|
24
22
|
break if [ 'quit', 'exit' ].include?(user_input.downcase)
|
25
23
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class BashTool < LlmGateway::Tool
|
2
|
+
name 'Bash'
|
3
|
+
description 'Execute shell commands'
|
4
|
+
input_schema({
|
5
|
+
type: 'object',
|
6
|
+
properties: {
|
7
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
8
|
+
description: { type: 'string', description: 'Human-readable description' },
|
9
|
+
timeout: { type: 'integer', description: 'Timeout in milliseconds' }
|
10
|
+
},
|
11
|
+
required: [ 'command' ]
|
12
|
+
})
|
13
|
+
|
14
|
+
def execute(input)
|
15
|
+
command = input[:command]
|
16
|
+
description = input[:description]
|
17
|
+
timeout = input[:timeout] || 120000 # Default 2 minutes
|
18
|
+
|
19
|
+
if description
|
20
|
+
puts "Executing: #{command}"
|
21
|
+
puts "Description: #{description}\n\n"
|
22
|
+
else
|
23
|
+
puts "Executing: #{command}\n\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
# Convert timeout from milliseconds to seconds
|
28
|
+
timeout_seconds = timeout / 1000.0
|
29
|
+
|
30
|
+
# Use timeout command if available, otherwise use Ruby's timeout
|
31
|
+
if system('which timeout > /dev/null 2>&1')
|
32
|
+
result = `timeout #{timeout_seconds}s #{command} 2>&1`
|
33
|
+
exit_status = $?
|
34
|
+
else
|
35
|
+
require 'timeout'
|
36
|
+
result = Timeout.timeout(timeout_seconds) do
|
37
|
+
`#{command} 2>&1`
|
38
|
+
end
|
39
|
+
exit_status = $?
|
40
|
+
end
|
41
|
+
|
42
|
+
if exit_status.success?
|
43
|
+
result.empty? ? "Command completed successfully (no output)" : result
|
44
|
+
else
|
45
|
+
"Command failed with exit code #{exit_status.exitstatus}:\n#{result}"
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue Timeout::Error
|
49
|
+
"Command timed out after #{timeout_seconds} seconds"
|
50
|
+
rescue => e
|
51
|
+
"Error executing command: #{e.message}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class EditTool < LlmGateway::Tool
|
2
|
+
name 'Edit'
|
3
|
+
description 'Modify existing files by replacing specific text strings'
|
4
|
+
input_schema({
|
5
|
+
type: 'object',
|
6
|
+
properties: {
|
7
|
+
file_path: { type: 'string', description: 'Absolute path to file to modify' },
|
8
|
+
old_string: { type: 'string', description: 'Exact text to replace' },
|
9
|
+
new_string: { type: 'string', description: 'Replacement text' },
|
10
|
+
replace_all: { type: 'boolean', description: 'Replace all occurrences (default: false)' }
|
11
|
+
},
|
12
|
+
required: [ 'file_path', 'old_string', 'new_string' ]
|
13
|
+
})
|
14
|
+
|
15
|
+
def execute(input)
|
16
|
+
file_path = input[:file_path]
|
17
|
+
old_string = input[:old_string]
|
18
|
+
new_string = input[:new_string]
|
19
|
+
replace_all = input[:replace_all] || false
|
20
|
+
|
21
|
+
# Validate file exists
|
22
|
+
unless File.exist?(file_path)
|
23
|
+
return "Error: File not found at #{file_path}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Read file content
|
27
|
+
begin
|
28
|
+
content = File.read(file_path)
|
29
|
+
rescue => e
|
30
|
+
return "Error reading file: #{e.message}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check if old_string exists in file
|
34
|
+
unless content.include?(old_string)
|
35
|
+
return "Error: Text '#{old_string}' not found in file"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Perform replacement
|
39
|
+
if replace_all
|
40
|
+
updated_content = content.gsub(old_string, new_string)
|
41
|
+
occurrences = content.scan(old_string).length
|
42
|
+
else
|
43
|
+
# Replace only first occurrence
|
44
|
+
updated_content = content.sub(old_string, new_string)
|
45
|
+
occurrences = 1
|
46
|
+
end
|
47
|
+
|
48
|
+
# Check if replacement would result in same content
|
49
|
+
if content == updated_content
|
50
|
+
return "Error: old_string and new_string are identical, no changes made"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Write updated content back to file
|
54
|
+
begin
|
55
|
+
File.write(file_path, updated_content)
|
56
|
+
"Successfully replaced #{occurrences} occurrence(s) in #{file_path}"
|
57
|
+
rescue => e
|
58
|
+
"Error writing file: #{e.message}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class GrepTool < LlmGateway::Tool
|
2
|
+
name 'Grep'
|
3
|
+
description 'Search for patterns in files using regex'
|
4
|
+
input_schema({
|
5
|
+
type: 'object',
|
6
|
+
properties: {
|
7
|
+
pattern: { type: 'string', description: 'Regex pattern to search for' },
|
8
|
+
path: { type: 'string', description: 'File or directory path' },
|
9
|
+
output_mode: {
|
10
|
+
type: 'string',
|
11
|
+
enum: [ 'content', 'files_with_matches', 'count' ],
|
12
|
+
description: 'Output mode: content, files_with_matches, or count'
|
13
|
+
},
|
14
|
+
glob: { type: 'string', description: 'File pattern filter (e.g., "*.rb")' },
|
15
|
+
'-n': { type: 'boolean', description: 'Show line numbers' },
|
16
|
+
'-i': { type: 'boolean', description: 'Case insensitive search' },
|
17
|
+
'-C': { type: 'integer', description: 'Context lines around matches' }
|
18
|
+
},
|
19
|
+
required: [ 'pattern' ]
|
20
|
+
})
|
21
|
+
|
22
|
+
def execute(input)
|
23
|
+
pattern = input[:pattern]
|
24
|
+
path = input[:path] || '.'
|
25
|
+
output_mode = input[:output_mode] || 'files_with_matches'
|
26
|
+
glob = input[:glob]
|
27
|
+
show_line_numbers = input['-n'] || false
|
28
|
+
case_insensitive = input['-i'] || false
|
29
|
+
context_lines = input['-C'] || 0
|
30
|
+
|
31
|
+
# Build grep command
|
32
|
+
cmd_parts = [ 'grep' ]
|
33
|
+
|
34
|
+
# Add flags
|
35
|
+
cmd_parts << '-r' unless File.file?(path) # Recursive for directories
|
36
|
+
cmd_parts << '-n' if show_line_numbers && output_mode == 'content'
|
37
|
+
cmd_parts << '-i' if case_insensitive
|
38
|
+
cmd_parts << "-C#{context_lines}" if context_lines > 0 && output_mode == 'content'
|
39
|
+
|
40
|
+
# Output mode flags
|
41
|
+
case output_mode
|
42
|
+
when 'files_with_matches'
|
43
|
+
cmd_parts << '-l'
|
44
|
+
when 'count'
|
45
|
+
cmd_parts << '-c'
|
46
|
+
end
|
47
|
+
|
48
|
+
# Add pattern and path
|
49
|
+
cmd_parts << "'#{pattern}'"
|
50
|
+
|
51
|
+
# Handle glob pattern
|
52
|
+
if glob
|
53
|
+
if File.directory?(path)
|
54
|
+
cmd_parts << "#{path}/**/*"
|
55
|
+
# Use shell globbing with find for better glob support
|
56
|
+
find_cmd = "find #{path} -name '#{glob}' -type f"
|
57
|
+
files_result = `#{find_cmd} 2>/dev/null`
|
58
|
+
if files_result.empty?
|
59
|
+
return "No files found matching pattern '#{glob}' in #{path}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Run grep on each matching file
|
63
|
+
files = files_result.strip.split("\n")
|
64
|
+
results = []
|
65
|
+
|
66
|
+
files.each do |file|
|
67
|
+
grep_cmd = cmd_parts[0..-2].join(' ') + " '#{pattern}' '#{file}'"
|
68
|
+
result = `#{grep_cmd} 2>/dev/null`
|
69
|
+
results << result unless result.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
return results.empty? ? "No matches found" : results.join("\n")
|
73
|
+
else
|
74
|
+
cmd_parts << path
|
75
|
+
end
|
76
|
+
else
|
77
|
+
cmd_parts << path
|
78
|
+
end
|
79
|
+
|
80
|
+
command = cmd_parts.join(' ')
|
81
|
+
|
82
|
+
begin
|
83
|
+
puts "Executing: #{command}"
|
84
|
+
result = `#{command} 2>&1`
|
85
|
+
exit_status = $?
|
86
|
+
|
87
|
+
if exit_status.success?
|
88
|
+
if result.empty?
|
89
|
+
"No matches found"
|
90
|
+
else
|
91
|
+
case output_mode
|
92
|
+
when 'content'
|
93
|
+
result
|
94
|
+
when 'files_with_matches'
|
95
|
+
result
|
96
|
+
when 'count'
|
97
|
+
result
|
98
|
+
else
|
99
|
+
result
|
100
|
+
end
|
101
|
+
end
|
102
|
+
elsif exit_status.exitstatus == 1
|
103
|
+
# grep returns 1 when no matches found, which is normal
|
104
|
+
"No matches found"
|
105
|
+
else
|
106
|
+
"Error: #{result}"
|
107
|
+
end
|
108
|
+
|
109
|
+
rescue => e
|
110
|
+
"Error executing grep: #{e.message}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class ReadTool < LlmGateway::Tool
|
2
|
+
name 'Read'
|
3
|
+
description 'Read file contents with optional pagination'
|
4
|
+
input_schema({
|
5
|
+
type: 'object',
|
6
|
+
properties: {
|
7
|
+
file_path: { type: 'string', description: 'Absolute path to file' },
|
8
|
+
limit: { type: 'integer', description: 'Number of lines to read' },
|
9
|
+
offset: { type: 'integer', description: 'Starting line number' }
|
10
|
+
},
|
11
|
+
required: [ 'file_path' ]
|
12
|
+
})
|
13
|
+
|
14
|
+
def execute(input)
|
15
|
+
file_path = input[:file_path]
|
16
|
+
limit = input[:limit]
|
17
|
+
offset = input[:offset] || 0
|
18
|
+
|
19
|
+
# Validate file exists
|
20
|
+
unless File.exist?(file_path)
|
21
|
+
return "Error: File not found at #{file_path}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Check if it's a directory
|
25
|
+
if File.directory?(file_path)
|
26
|
+
return "Error: #{file_path} is a directory, not a file"
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
lines = File.readlines(file_path, chomp: true)
|
31
|
+
|
32
|
+
# Apply offset
|
33
|
+
if offset > 0
|
34
|
+
if offset >= lines.length
|
35
|
+
return "Error: Offset #{offset} exceeds file length (#{lines.length} lines)"
|
36
|
+
end
|
37
|
+
lines = lines[offset..-1]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Apply limit
|
41
|
+
if limit && limit > 0
|
42
|
+
lines = lines[0, limit]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Format output with line numbers (similar to cat -n)
|
46
|
+
output = lines.each_with_index.map do |line, index|
|
47
|
+
line_number = offset + index + 1
|
48
|
+
"#{line_number.to_s.rjust(6)}→#{line}"
|
49
|
+
end
|
50
|
+
|
51
|
+
if output.empty?
|
52
|
+
"File is empty or no lines in specified range"
|
53
|
+
else
|
54
|
+
output.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
rescue => e
|
58
|
+
"Error reading file: #{e.message}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class TodoWriteTool < LlmGateway::Tool
|
4
|
+
name 'TodoWrite'
|
5
|
+
description 'Create and manage structured task lists'
|
6
|
+
input_schema({
|
7
|
+
type: 'object',
|
8
|
+
properties: {
|
9
|
+
todos: {
|
10
|
+
type: 'array',
|
11
|
+
description: 'Array of todo objects',
|
12
|
+
items: {
|
13
|
+
type: 'object',
|
14
|
+
properties: {
|
15
|
+
id: { type: 'string', description: 'Unique identifier' },
|
16
|
+
content: { type: 'string', description: 'Task description' },
|
17
|
+
status: {
|
18
|
+
type: 'string',
|
19
|
+
enum: [ 'pending', 'in_progress', 'completed' ],
|
20
|
+
description: 'Task status'
|
21
|
+
},
|
22
|
+
priority: {
|
23
|
+
type: 'string',
|
24
|
+
enum: [ 'high', 'medium', 'low' ],
|
25
|
+
description: 'Task priority'
|
26
|
+
}
|
27
|
+
},
|
28
|
+
required: [ 'id', 'content', 'status', 'priority' ]
|
29
|
+
}
|
30
|
+
}
|
31
|
+
},
|
32
|
+
required: [ 'todos' ]
|
33
|
+
})
|
34
|
+
|
35
|
+
def execute(input)
|
36
|
+
todos = input[:todos]
|
37
|
+
|
38
|
+
# Validate todos structure
|
39
|
+
todos.each_with_index do |todo, index|
|
40
|
+
unless todo.is_a?(Hash)
|
41
|
+
return "Error: Todo at index #{index} is not a hash"
|
42
|
+
end
|
43
|
+
|
44
|
+
required_fields = [ 'id', 'content', 'status', 'priority' ]
|
45
|
+
missing_fields = required_fields - todo.keys.map(&:to_s)
|
46
|
+
unless missing_fields.empty?
|
47
|
+
return "Error: Todo at index #{index} missing required fields: #{missing_fields.join(', ')}"
|
48
|
+
end
|
49
|
+
|
50
|
+
valid_statuses = [ 'pending', 'in_progress', 'completed' ]
|
51
|
+
unless valid_statuses.include?(todo['status'])
|
52
|
+
return "Error: Invalid status '#{todo['status']}' in todo #{todo['id']}. Must be one of: #{valid_statuses.join(', ')}"
|
53
|
+
end
|
54
|
+
|
55
|
+
valid_priorities = [ 'high', 'medium', 'low' ]
|
56
|
+
unless valid_priorities.include?(todo['priority'])
|
57
|
+
return "Error: Invalid priority '#{todo['priority']}' in todo #{todo['id']}. Must be one of: #{valid_priorities.join(', ')}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Store todos (in practice, this might be saved to a file or database)
|
62
|
+
@todos = todos
|
63
|
+
|
64
|
+
# Generate summary
|
65
|
+
total = todos.length
|
66
|
+
pending = todos.count { |t| t['status'] == 'pending' }
|
67
|
+
in_progress = todos.count { |t| t['status'] == 'in_progress' }
|
68
|
+
completed = todos.count { |t| t['status'] == 'completed' }
|
69
|
+
|
70
|
+
summary = "Todo list updated successfully:\n"
|
71
|
+
summary += "Total tasks: #{total}\n"
|
72
|
+
summary += "Pending: #{pending}, In Progress: #{in_progress}, Completed: #{completed}\n\n"
|
73
|
+
|
74
|
+
# List current todos
|
75
|
+
summary += "Current tasks:\n"
|
76
|
+
todos.each do |todo|
|
77
|
+
status_icon = case todo['status']
|
78
|
+
when 'pending' then '⏳'
|
79
|
+
when 'in_progress' then '🔄'
|
80
|
+
when 'completed' then '✅'
|
81
|
+
end
|
82
|
+
|
83
|
+
priority_icon = case todo['priority']
|
84
|
+
when 'high' then '🔴'
|
85
|
+
when 'medium' then '🟡'
|
86
|
+
when 'low' then '🟢'
|
87
|
+
end
|
88
|
+
|
89
|
+
summary += "#{status_icon} #{priority_icon} [#{todo['id']}] #{todo['content']}\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
summary
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.current_todos
|
96
|
+
@todos || []
|
97
|
+
end
|
98
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: llm_gateway
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- billybonks
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: LlmGateway provides a consistent Ruby interface for multiple LLM providers
|
14
14
|
including Claude, OpenAI, and Groq. Features include unified response formatting,
|
@@ -43,10 +43,15 @@ files:
|
|
43
43
|
- lib/llm_gateway/tool.rb
|
44
44
|
- lib/llm_gateway/utils.rb
|
45
45
|
- lib/llm_gateway/version.rb
|
46
|
-
- sample/
|
47
|
-
- sample/
|
48
|
-
- sample/
|
49
|
-
- sample/
|
46
|
+
- sample/claude_code_clone/agent.rb
|
47
|
+
- sample/claude_code_clone/claude_code_clone.rb
|
48
|
+
- sample/claude_code_clone/prompt.rb
|
49
|
+
- sample/claude_code_clone/run.rb
|
50
|
+
- sample/claude_code_clone/tools/bash_tool.rb
|
51
|
+
- sample/claude_code_clone/tools/edit_tool.rb
|
52
|
+
- sample/claude_code_clone/tools/grep_tool.rb
|
53
|
+
- sample/claude_code_clone/tools/read_tool.rb
|
54
|
+
- sample/claude_code_clone/tools/todowrite_tool.rb
|
50
55
|
- sig/llm_gateway.rbs
|
51
56
|
homepage: https://github.com/Hyper-Unearthing/llm_gateway
|
52
57
|
licenses:
|
@@ -1,72 +0,0 @@
|
|
1
|
-
require_relative 'file_search_tool'
|
2
|
-
require_relative 'file_search_prompt'
|
3
|
-
require 'debug'
|
4
|
-
|
5
|
-
# Bash File Search Assistant using LlmGateway architecture
|
6
|
-
|
7
|
-
class FileSearchBot
|
8
|
-
def initialize(model, api_key)
|
9
|
-
@transcript = []
|
10
|
-
@model = model
|
11
|
-
@api_key = api_key
|
12
|
-
end
|
13
|
-
|
14
|
-
def query(input)
|
15
|
-
process_query(input)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def process_query(query)
|
21
|
-
# Add user message to transcript
|
22
|
-
@transcript << { role: 'user', content: [ { type: 'text', text: query } ] }
|
23
|
-
|
24
|
-
begin
|
25
|
-
prompt = FileSearchPrompt.new(@model, @transcript, @api_key)
|
26
|
-
result = prompt.post
|
27
|
-
process_response(result[:choices][0][:content])
|
28
|
-
rescue => e
|
29
|
-
puts "Error: #{e.message}"
|
30
|
-
puts "Backtrace: #{e.backtrace.join("\n")}"
|
31
|
-
puts "I give up as bot"
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def process_response(response)
|
36
|
-
# Add assistant response to transcript
|
37
|
-
@transcript << { role: 'assistant', content: response }
|
38
|
-
|
39
|
-
response.each do |message|
|
40
|
-
if message[:type] == 'text'
|
41
|
-
puts "\nBot: #{message[:text]}\n"
|
42
|
-
elsif message[:type] == 'tool_use'
|
43
|
-
result = handle_tool_use(message)
|
44
|
-
|
45
|
-
# Add tool result to transcript
|
46
|
-
tool_result = {
|
47
|
-
type: 'tool_result',
|
48
|
-
tool_use_id: message[:id],
|
49
|
-
content: result
|
50
|
-
}
|
51
|
-
@transcript << { role: 'user', content: [ tool_result ] }
|
52
|
-
|
53
|
-
# Continue conversation with tool result
|
54
|
-
follow_up_prompt = FileSearchPrompt.new(@model, @transcript, @api_key)
|
55
|
-
follow_up = follow_up_prompt.post
|
56
|
-
|
57
|
-
process_response(follow_up[:choices][0][:content]) if follow_up[:choices][0][:content]
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def handle_tool_use(message)
|
63
|
-
if message[:name] == 'execute_bash_command'
|
64
|
-
tool = FileSearchTool.new
|
65
|
-
tool.execute(message[:input])
|
66
|
-
else
|
67
|
-
"Unknown tool: #{message[:name]}"
|
68
|
-
end
|
69
|
-
rescue StandardError => e
|
70
|
-
"Error executing tool: #{e.message}"
|
71
|
-
end
|
72
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
require_relative 'file_search_tool'
|
2
|
-
|
3
|
-
class FileSearchPrompt < LlmGateway::Prompt
|
4
|
-
def initialize(model, transcript, api_key)
|
5
|
-
super(model)
|
6
|
-
@transcript = transcript
|
7
|
-
@api_key = api_key
|
8
|
-
end
|
9
|
-
|
10
|
-
def prompt
|
11
|
-
@transcript
|
12
|
-
end
|
13
|
-
|
14
|
-
def system_prompt
|
15
|
-
<<~SYSTEM
|
16
|
-
You are a helpful assistant that can find things for them in directories.
|
17
|
-
|
18
|
-
# Bash File Search Assistant
|
19
|
-
|
20
|
-
You are a bash command-line assistant specialized in helping users find information in files and directories. Your role is to translate natural language queries into effective bash commands using search and file inspection tools.
|
21
|
-
|
22
|
-
## Available Commands
|
23
|
-
|
24
|
-
You have access to these bash commands:
|
25
|
-
- `find` - Locate files and directories by name, type, size, date, etc.
|
26
|
-
- `grep` - Search for text patterns within files
|
27
|
-
- `cat` - Display entire file contents
|
28
|
-
- `head` - Show first lines of files
|
29
|
-
- `tail` - Show last lines of files
|
30
|
-
- `ls` - List directory contents with various options
|
31
|
-
- `wc` - Count lines, words, characters
|
32
|
-
- `sort` - Sort file contents
|
33
|
-
- `uniq` - Remove duplicate lines
|
34
|
-
- `awk` - Text processing and pattern extraction
|
35
|
-
- `sed` - Stream editing and text manipulation
|
36
|
-
|
37
|
-
## Your Process
|
38
|
-
|
39
|
-
1. **Understand the Query**: Parse what the user is looking for
|
40
|
-
2. **Choose Strategy**: Determine the best combination of commands
|
41
|
-
3. **Execute Commands**: Use the execute_bash_command tool with exact bash commands
|
42
|
-
4. **Explain**: Briefly explain what each command does
|
43
|
-
5. **Suggest Refinements**: Offer ways to narrow or expand the search if needed
|
44
|
-
|
45
|
-
Always use the execute_bash_command tool to run commands rather than just suggesting them.
|
46
|
-
SYSTEM
|
47
|
-
end
|
48
|
-
|
49
|
-
def tools
|
50
|
-
[ FileSearchTool.definition ]
|
51
|
-
end
|
52
|
-
|
53
|
-
def post
|
54
|
-
LlmGateway::Client.chat(model, prompt, tools: tools, system: system_prompt, api_key: @api_key)
|
55
|
-
end
|
56
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
|
2
|
-
class FileSearchTool < LlmGateway::Tool
|
3
|
-
name 'execute_bash_command'
|
4
|
-
description 'Execute bash commands for file searching and directory exploration'
|
5
|
-
input_schema({
|
6
|
-
type: 'object',
|
7
|
-
properties: {
|
8
|
-
command: { type: 'string', description: 'The bash command to execute' },
|
9
|
-
explanation: { type: 'string', description: 'Explanation of what the command does' }
|
10
|
-
},
|
11
|
-
required: [ 'command', 'explanation' ]
|
12
|
-
})
|
13
|
-
|
14
|
-
def execute(input, login = nil)
|
15
|
-
command = input[:command]
|
16
|
-
explanation = input[:explanation]
|
17
|
-
|
18
|
-
puts "Executing: #{command}"
|
19
|
-
puts "Purpose: #{explanation}\n\n"
|
20
|
-
|
21
|
-
begin
|
22
|
-
result = `#{command} 2>&1`
|
23
|
-
if $?.success?
|
24
|
-
result.empty? ? "Command completed successfully (no output)" : result
|
25
|
-
else
|
26
|
-
"Error: #{result}"
|
27
|
-
end
|
28
|
-
rescue => e
|
29
|
-
"Error executing command: #{e.message}"
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|