llm_gateway 0.3.0 → 0.4.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 +26 -0
- data/README.md +544 -186
- data/Rakefile +1 -2
- data/docs/migration-guide.md +135 -0
- data/lib/llm_gateway/adapters/adapter.rb +173 -0
- data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +23 -0
- data/lib/llm_gateway/adapters/{claude → anthropic}/bidirectional_message_mapper.rb +31 -3
- data/lib/llm_gateway/adapters/{claude → anthropic}/input_mapper.rb +4 -3
- data/lib/llm_gateway/adapters/anthropic/messages_adapter.rb +19 -0
- data/lib/llm_gateway/adapters/{claude → anthropic}/output_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +110 -0
- data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +53 -0
- data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +47 -0
- data/lib/llm_gateway/adapters/groq/option_mapper.rb +27 -0
- data/lib/llm_gateway/adapters/input_message_sanitizer.rb +93 -0
- data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +22 -0
- data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +31 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/bidirectional_message_mapper.rb +9 -2
- data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/input_mapper.rb +1 -6
- data/lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb +65 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +39 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/output_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +242 -0
- data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +20 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/file_output_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb +39 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/responses/bidirectional_message_mapper.rb +52 -4
- data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +106 -0
- data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +41 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/responses/output_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +340 -0
- data/lib/llm_gateway/adapters/openai/responses_adapter.rb +20 -0
- data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +206 -0
- data/lib/llm_gateway/adapters/openai_codex/option_mapper.rb +28 -0
- data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +38 -0
- data/lib/llm_gateway/adapters/option_mapper.rb +13 -0
- data/lib/llm_gateway/adapters/stream_accumulator.rb +91 -0
- data/lib/llm_gateway/adapters/structs.rb +145 -0
- data/lib/llm_gateway/base_client.rb +62 -1
- data/lib/llm_gateway/client.rb +45 -129
- data/lib/llm_gateway/clients/anthropic.rb +167 -0
- data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +162 -0
- data/lib/llm_gateway/clients/claude_code/token_manager.rb +112 -0
- data/lib/llm_gateway/clients/groq.rb +54 -0
- data/lib/llm_gateway/clients/openai.rb +208 -0
- data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +258 -0
- data/lib/llm_gateway/clients/openai_codex/token_manager.rb +71 -0
- data/lib/llm_gateway/errors.rb +21 -0
- data/lib/llm_gateway/prompt.rb +12 -1
- data/lib/llm_gateway/provider_registry.rb +37 -0
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +165 -14
- data/scripts/create_anthropic_credentials.rb +106 -0
- data/scripts/create_openai_codex_credentials.rb +116 -0
- data/scripts/generate_handoff_live_fixture.rb +169 -0
- data/scripts/generate_handoff_media_fixture.rb +167 -0
- metadata +64 -28
- data/lib/llm_gateway/adapters/claude/client.rb +0 -60
- data/lib/llm_gateway/adapters/groq/bidirectional_message_mapper.rb +0 -18
- data/lib/llm_gateway/adapters/groq/client.rb +0 -58
- data/lib/llm_gateway/adapters/groq/input_mapper.rb +0 -18
- data/lib/llm_gateway/adapters/groq/output_mapper.rb +0 -10
- data/lib/llm_gateway/adapters/open_ai/client.rb +0 -80
- data/lib/llm_gateway/adapters/open_ai/responses/input_mapper.rb +0 -62
- data/sample/claude_code_clone/agent.rb +0 -65
- data/sample/claude_code_clone/claude_code_clone.rb +0 -40
- data/sample/claude_code_clone/prompt.rb +0 -79
- data/sample/claude_code_clone/run.rb +0 -47
- data/sample/claude_code_clone/tools/bash_tool.rb +0 -54
- data/sample/claude_code_clone/tools/edit_tool.rb +0 -61
- data/sample/claude_code_clone/tools/grep_tool.rb +0 -113
- data/sample/claude_code_clone/tools/read_tool.rb +0 -61
- data/sample/claude_code_clone/tools/todowrite_tool.rb +0 -98
|
@@ -1,40 +0,0 @@
|
|
|
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
|
|
@@ -1,79 +0,0 @@
|
|
|
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,47 +0,0 @@
|
|
|
1
|
-
require 'tty-prompt'
|
|
2
|
-
require_relative '../../lib/llm_gateway'
|
|
3
|
-
require_relative 'claude_code_clone.rb'
|
|
4
|
-
|
|
5
|
-
# Terminal Runner for FileSearchBot
|
|
6
|
-
class FileSearchTerminalRunner
|
|
7
|
-
def initialize
|
|
8
|
-
@prompt = TTY::Prompt.new
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def start
|
|
12
|
-
puts "First, let's configure your LLM settings:\n\n"
|
|
13
|
-
|
|
14
|
-
model, api_key = setup_configuration
|
|
15
|
-
bot = ClaudeCloneClone.new(model, api_key)
|
|
16
|
-
|
|
17
|
-
puts "Type 'quit' or 'exit' to stop.\n\n"
|
|
18
|
-
|
|
19
|
-
loop do
|
|
20
|
-
user_input = @prompt.ask("What can i do for you?")
|
|
21
|
-
|
|
22
|
-
break if [ 'quit', 'exit' ].include?(user_input.downcase)
|
|
23
|
-
|
|
24
|
-
bot.query(user_input)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def setup_configuration
|
|
31
|
-
model = @prompt.ask("Enter model (default: claude-3-7-sonnet-20250219):") do |q|
|
|
32
|
-
q.default 'claude-3-7-sonnet-20250219'
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
api_key = @prompt.mask("Enter your API key:") do |q|
|
|
36
|
-
q.required true
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
[ model, api_key ]
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Start the bot
|
|
44
|
-
if __FILE__ == $0
|
|
45
|
-
runner = FileSearchTerminalRunner.new
|
|
46
|
-
runner.start
|
|
47
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
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
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
|
@@ -1,113 +0,0 @@
|
|
|
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
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|