llm_gateway 0.1.4 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fc0c0030bd149c9e3130d1f8e6fe2ccd0bfec5d87931534e4fc29d8808ad0c7
4
- data.tar.gz: 1c9bf69153cfc502b41ef27305fcba114908a9debcdc34e9e2023fef257b4968
3
+ metadata.gz: 86c8c0937c6d8d78b1b4b3c2dfa1ed749fbd74eb40e52ccf084dbf0e3cccbb8e
4
+ data.tar.gz: 682021570b7d903ba44bfc606a9ca9e9fc45ce897bb8637a32d563bfd5497de4
5
5
  SHA512:
6
- metadata.gz: 27ee426f1d8c3b97054e44b0d270d4eb8dd6d263fdf0717b1a47d655758ae634d1e25c12a850e468f4d360ca00beecfc430c9bfd3ed7a0a287bd0a48febcb110
7
- data.tar.gz: fbc9b70052a776a99962b173842ede5016f4398504fca58d163a60f3eda52c58b2434c91d060bec3334c8c38e083c313692fb6b5442792943fcd3008ee44a08a
6
+ metadata.gz: b684e11152959b054bb30213982845e0978dfe91a2473de7e2a326ca37d2c9e6ec8411be1b5a729e3d99bfb0654bfc420bacf7b752219286295cf80d2c1245f1
7
+ data.tar.gz: 3da1cf5fcc649024b9e08859905430e1a89082333ec614c39b6fd22ff360143fe5be5f1efb9d7ddf81e8d9f08e62549c23ae1daadc8ef82e234fb6e433a4b899
data/CHANGELOG.md CHANGED
@@ -1,8 +1,27 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased](https://github.com/Hyper-Unearthing/llm_gateway/tree/HEAD)
3
+ ## [v0.1.6](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.1.6) (2025-08-05)
4
4
 
5
- [Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.3...HEAD)
5
+ [Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.5...v0.1.6)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - Fix gem release task commit message interpolation [\#12](https://github.com/Hyper-Unearthing/llm_gateway/pull/12) ([billybonks](https://github.com/billybonks))
10
+
11
+ ## [v0.1.5](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.1.5) (2025-08-05)
12
+
13
+ [Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.4...v0.1.5)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - burn: login from tool base class [\#11](https://github.com/Hyper-Unearthing/llm_gateway/pull/11) ([billybonks](https://github.com/billybonks))
18
+ - improve sample [\#10](https://github.com/Hyper-Unearthing/llm_gateway/pull/10) ([billybonks](https://github.com/billybonks))
19
+ - ci: mark latest change log as a version [\#9](https://github.com/Hyper-Unearthing/llm_gateway/pull/9) ([billybonks](https://github.com/billybonks))
20
+ - 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))
21
+
22
+ ## [v0.1.4](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.1.4) (2025-08-04)
23
+
24
+ [Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.3...v0.1.4)
6
25
 
7
26
  **Merged pull requests:**
8
27
 
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/directory_bot/) for a complete working application that demonstrates:
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/directory_bot
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
- # Generate changelog first
18
- sh "bundle exec github_changelog_generator -u Hyper-Unearthing -p llm_gateway"
19
- sh "git add CHANGELOG.md"
20
- sh "git commit -m 'Update changelog' || echo 'No changelog changes'"
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
- # Release
32
- sh "gem bump --version #{version_type} --tag --push --release"
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 #{new_version}\""
64
+
65
+ # Tag and push
66
+ sh "git tag v#{new_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
@@ -39,7 +39,7 @@ module LlmGateway
39
39
  definition[:name]
40
40
  end
41
41
 
42
- def execute(input, login)
42
+ def execute(input)
43
43
  raise NotImplementedError, "Subclasses must implement execute"
44
44
  end
45
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmGateway
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
  end
@@ -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 'file_search_bot'
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 = FileSearchBot.new(model, api_key)
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 would you like to find?")
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
4
+ version: 0.1.6
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-04 00:00:00.000000000 Z
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/directory_bot/file_search_bot.rb
47
- - sample/directory_bot/file_search_prompt.rb
48
- - sample/directory_bot/file_search_tool.rb
49
- - sample/directory_bot/run.rb
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