llm-shell 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bad036ff98c154b18cabda0e2b598b40c242907e6eda28b6d2cfaa1fba66a265
4
+ data.tar.gz: 85d5fbc076495609562221a2296eabb06ec03c422ce1a2ea48661a3cf23213e9
5
+ SHA512:
6
+ metadata.gz: 182781650d0281008f8741ef389b1c7eaaf815191df2a3e8153415d6b65fdf64ce65d5d83ec67debfd05bde2f408f40c9ad79a1f21299dac699ba7c24f580f74
7
+ data.tar.gz: cebfa126c00d63d9927a9cfd6684f382016bd2c805aadf0da3ae5798d4bc136ccea3682af9c3c4e89236cfcbb7a837891b092a3744e537581eb676de9ecbaa9d
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ ## About
2
+
3
+ llm-shell is an extensible, developer-oriented command-line
4
+ utility that can interact with multiple Large Language Models
5
+ (LLMs). It serves as both a demo of the [llmrb/llm](https://github.com/llmrb/llm)
6
+ library and a tool to help improve the library through real-world
7
+ usage and feedback. Jump to the [Demos](#demos) section to see
8
+ it in action!
9
+
10
+ ## Features
11
+
12
+ - 🌟 Unified interface for multiple Large Language Models (LLMs)
13
+ - 🤝 Supports Gemini, OpenAI, Anthropic, and Ollama
14
+ - 📤 Attach local files as conversation context
15
+ - 🔧 Extend with your own functions and tool calls
16
+ - 📝 Advanced Markdown formatting and output
17
+
18
+ ## Demos
19
+
20
+ <details>
21
+ <summary><b>1. Tool calls</b></summary>
22
+ <img src="share/llm-shell/examples/example2.gif/">
23
+ </details>
24
+
25
+ <details>
26
+ <summary><b>2. File discussion</b></summary>
27
+ <img src="share/llm-shell/examples/example1.gif">
28
+ </details>
29
+
30
+ ## Customization
31
+
32
+ #### Functions
33
+
34
+ The `~/.llm-shell/tools/` directory can contain one or more
35
+ [llmrb/llm](https://github.com/llmrb/llm) functions that the
36
+ LLM can call once you confirm you are okay with executing the
37
+ code locally (along with any arguments it provides). See the
38
+ earlier demo for an example.
39
+
40
+ For security and safety reasons, a user must confirm the execution of
41
+ all function calls before they happen and also add the function to
42
+ an allowlist before it will be loaded by llm-shell automatically
43
+ at boot time. See below for more details on how this can be done.
44
+
45
+ An LLM function generally looks like this, and it can be dropped
46
+ into the `~/.llm-shell/tools/` directory. This function is the one
47
+ from the demo earlier, and I saved it as `~/.llm-shell/tools/system.rb`.
48
+ The function's return value is relayed back to the LLM.
49
+
50
+
51
+ ```ruby
52
+ LLM.function(:system) do |fn|
53
+ fn.description "Run a shell command"
54
+ fn.params do |schema|
55
+ schema.object(command: schema.string.required)
56
+ end
57
+ fn.define do |params|
58
+ `#{params.command}`
59
+ end
60
+ end
61
+ ```
62
+
63
+ ## Settings
64
+
65
+ #### YAML
66
+
67
+ The console client can be configured at the command line through option switches,
68
+ or through a YAML file. The YAML file can contain the same options that could be
69
+ specified at the command line. For cloud providers the key option is the only
70
+ required parameter, everything else has defaults. The YAML file is read from the
71
+ path `${HOME}/.llm-shell/config.yml` and it has the following format:
72
+
73
+ ```yaml
74
+ # ~/.config/llm-shell.yml
75
+ openai:
76
+ key: YOURKEY
77
+ model: gpt-4o-mini
78
+ gemini:
79
+ key: YOURKEY
80
+ model: gemini-2.0-flash-001
81
+ anthropic:
82
+ key: YOURKEY
83
+ model: claude-3-7-sonnet-20250219
84
+ ollama:
85
+ host: localhost
86
+ model: deepseek-coder:6.7b
87
+ tools:
88
+ - system
89
+ ```
90
+
91
+ ## Usage
92
+
93
+ #### CLI
94
+
95
+ ```bash
96
+ Usage: llm-shell [OPTIONS]
97
+ -p, --provider NAME Required. Options: gemini, openai, anthropic, or ollama.
98
+ -k, --key [KEY] Optional. Required by gemini, openai, and anthropic.
99
+ -m, --model [MODEL] Optional. The name of a model.
100
+ -h, --host [HOST] Optional. Sometimes required by ollama.
101
+ -o, --port [PORT] Optional. Sometimes required by ollama.
102
+ -f, --files [GLOB] Optional. Glob pattern(s) separated by a comma.
103
+ -t, --tools [TOOLS] Optional. One or more tool names to load automatically.
104
+ ```
105
+
106
+ ## Install
107
+
108
+ llm-shell can be installed via [rubygems.org](https://rubygems.org/gems/llm-shell)
109
+
110
+ gem install llm-shell
111
+
112
+ ## License
113
+
114
+ [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
115
+ <br>
116
+ See [LICENSE](./LICENSE)
data/bin/llm-shell ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ def wait
5
+ Process.wait
6
+ rescue Interrupt
7
+ retry
8
+ end
9
+
10
+ def libexec
11
+ File.realpath File.join(__dir__, "..", "libexec", "llm-shell")
12
+ end
13
+
14
+ def main(argv)
15
+ Process.spawn File.join(libexec, "shell"), *ARGV[0..]
16
+ Process.wait
17
+ rescue Interrupt
18
+ wait
19
+ end
20
+ main(ARGV)
data/lib/io/line.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class IO::Line
4
+ require "io/console"
5
+
6
+ attr_reader :io
7
+
8
+ def initialize(io)
9
+ @io = io
10
+ end
11
+
12
+ def print(*strs)
13
+ tap { @io.print(strs.join.gsub($/, "")) }
14
+ end
15
+
16
+ def end
17
+ tap { @io.print($/) }
18
+ end
19
+
20
+ def rewind
21
+ tap do
22
+ @io.erase_line(2)
23
+ @io.goto_column(0)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Shell
4
+ class Config
5
+ ##
6
+ # @param [String] provider
7
+ # @return [LLM::Shell::Config]
8
+ def initialize(provider)
9
+ @provider = provider
10
+ end
11
+
12
+ ##
13
+ # @return [Hash]
14
+ def merge(other)
15
+ to_h.merge(other)
16
+ end
17
+
18
+ ##
19
+ # @return [Hash]
20
+ def to_h
21
+ yaml[@provider] || {}
22
+ end
23
+
24
+ private
25
+
26
+ def yaml
27
+ return {} unless File.readable?(path)
28
+ @yaml ||= YAML.load_file(path)
29
+ end
30
+
31
+ def path
32
+ File.join LLM::Shell.home, "config.yml"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Shell
4
+ class Default
5
+ def initialize(provider)
6
+ @provider = provider
7
+ end
8
+
9
+ def prompt
10
+ "You are a helpful assistant." \
11
+ "Answer the user's questions as best as you can." \
12
+ "The user's environment is a terminal." \
13
+ "Provide short and concise answers that are suitable for a terminal." \
14
+ "Do not provide long answers." \
15
+ "One or more files might be provided at the start of the conversation. " \
16
+ "The user might ask you about them, you should try to understand them and what they are. " \
17
+ "If you don't understand something, say so. " \
18
+ "Respond in markdown format." \
19
+ "Each file will be surrounded by the following markers: " \
20
+ "'# START: /path/to/file'" \
21
+ "'# END: /path/to/file'"
22
+ end
23
+
24
+ def role
25
+ case @provider
26
+ when "openai", "ollama" then :system
27
+ else :user
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Shell
4
+ class Formatter
5
+ FormatError = Class.new(RuntimeError)
6
+
7
+ def initialize(messages)
8
+ @messages = messages.reject(&:tool_call?)
9
+ end
10
+
11
+ def format!(role)
12
+ case role
13
+ when :user then format_user(messages)
14
+ when :assistant then format_assistant(messages)
15
+ else raise FormatError.new("#{role} is not known")
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :messages
22
+
23
+ def format_user(messages)
24
+ messages.flat_map do |message|
25
+ next unless message.user?
26
+ next unless String === message.content
27
+ role = Paint[message.role, :bold, :yellow]
28
+ title = "#{role} says: "
29
+ body = wrap(message.tap(&:read!).content)
30
+ [title, render(body), ""].join("\n")
31
+ end.join
32
+ end
33
+
34
+ def format_assistant(messages)
35
+ messages.flat_map do |message|
36
+ next unless message.assistant?
37
+ next unless String === message.content
38
+ role = Paint[message.role, :bold, :green]
39
+ title = "#{role} says: "
40
+ body = wrap(message.tap(&:read!).content)
41
+ [title, render(body)].join("\n")
42
+ end.join
43
+ end
44
+
45
+ def render(text)
46
+ Markdown.new(text).to_ansi
47
+ end
48
+
49
+ def wrap(text, width = 80)
50
+ text.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Shell
4
+ class Markdown
5
+ require "kramdown"
6
+
7
+ ##
8
+ # @param [String] text
9
+ # @return [LLM::Shell::Markdown]
10
+ def initialize(text)
11
+ @document = Kramdown::Document.new preprocessor(text)
12
+ end
13
+
14
+ ##
15
+ # @return [String]
16
+ def to_ansi
17
+ @document.root.children.map { |node| visit(node) }.join("\n")
18
+ end
19
+
20
+ private
21
+
22
+ def visit(node)
23
+ case node.type
24
+ when :header
25
+ level = node.options[:level]
26
+ color = levels[level]
27
+ Paint[("#" * level) + " " + node.children.map { visit(_1) }.join, color]
28
+ when :p
29
+ node.children.map { visit(_1) }.join
30
+ when :ul
31
+ node.children.map { visit(_1) }.join("\n")
32
+ when :li
33
+ "• " + node.children.map { visit(_1) }.join
34
+ when :em
35
+ Paint[node.children.map { visit(_1) }.join, :italic]
36
+ when :strong
37
+ Paint[node.children.map { visit(_1) }.join, :bold]
38
+ when :br
39
+ "\n"
40
+ when :text, :codespan
41
+ node.value
42
+ else
43
+ node.children.map { visit(_1) }.join
44
+ end
45
+ end
46
+
47
+ def levels
48
+ {
49
+ 1 => :green, 2 => :blue, 3 => :green,
50
+ 4 => :yellow, 5 => :red, 6 => :purple
51
+ }
52
+ end
53
+
54
+ def preprocessor(text)
55
+ text
56
+ .gsub(/([^\n])\n(#+ )/, "\\1\n\n\\2")
57
+ .gsub(/(#+ .+?)\n(?!\n)/, "\\1\n\n")
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Shell
4
+ ##
5
+ # The {LLM::Shell::Options LLM::Shell::Options} class represents
6
+ # the options provided to the shell at the command line, and the
7
+ # configuration file (if any). The command-line options take precedence
8
+ # over the configuration file.
9
+ class Options
10
+ ##
11
+ # @param [Hash] options
12
+ # @param [LLM::Shell::Default] default
13
+ # @return [LLM::Shell::Options]
14
+ def initialize(options, default)
15
+ @options = options.transform_keys(&:to_sym)
16
+ @provider = @options.delete(:provider)
17
+ @tools = @options.delete(:tools)
18
+ @files = Dir[*@options.delete(:files) || []].reject { File.directory?(_1) }
19
+ @chat_options = {model: @options.delete(:model)}.compact
20
+ @default = default
21
+ end
22
+
23
+ def provider = @provider
24
+ def tools = @tools
25
+ def files = @files
26
+ def llm = @options
27
+ def chat = @chat_options
28
+ def default = @default
29
+ end
30
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Shell
4
+ ##
5
+ # The {LLM::Shell::REPL LLM::Shell::REPL} class represents a loop
6
+ # that accepts user input, evaluates it via the LLM, and prints the
7
+ # response to stdout.
8
+ class REPL
9
+ ##
10
+ # @param [LLM::Chat] bot
11
+ # @param [LLM::Shell::Options] options
12
+ # @return [LLM::Shell::REPL]
13
+ def initialize(bot, options:)
14
+ @bot = bot
15
+ @console = IO.console
16
+ @options = options
17
+ @line = IO::Line.new($stdout)
18
+ end
19
+
20
+ ##
21
+ # Performs initial setup
22
+ # @return [void]
23
+ def setup
24
+ chat options.default.prompt, role: options.default.role
25
+ files.each { bot.chat ["# START: #{_1}", File.read(_1), "# END: #{_1}"].join("\n") }
26
+ bot.messages.each(&:read!)
27
+ clear_screen
28
+ end
29
+
30
+ ##
31
+ # Enters the main loop
32
+ # @return [void]
33
+ def start
34
+ loop do
35
+ read
36
+ eval
37
+ emit
38
+ rescue LLM::Error::ResponseError => ex
39
+ print Paint[ex.response.class, :red], "\n"
40
+ print ex.response.body, "\n"
41
+ rescue => ex
42
+ print Paint[ex.class, :red], "\n"
43
+ print ex.message, "\n"
44
+ print ex.backtrace[0..5].join("\n")
45
+ rescue Interrupt
46
+ throw(:exit, 0)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :bot, :console,
53
+ :line, :default,
54
+ :options
55
+
56
+ def formatter(messages) = Formatter.new(messages)
57
+ def unread = bot.messages.unread
58
+ def functions = bot.functions
59
+ def files = @options.files
60
+ def clear_screen = console.clear_screen
61
+
62
+ def read
63
+ input = Readline.readline("llm> ", true) || throw(:exit, 0)
64
+ chat input.tap { clear_screen }
65
+ line.rewind.print(Paint["Thinking", :bold])
66
+ unread.tap { line.rewind }
67
+ end
68
+
69
+ def eval
70
+ functions.each do |function|
71
+ print Paint["system", :bold, :red], " says: ", "\n"
72
+ print "function: ", function.name, "\n"
73
+ print "arguments: ", function.arguments, "\n"
74
+ print "Do you want to call it? "
75
+ input = $stdin.gets.chomp.downcase
76
+ puts
77
+ if %w(y yes yeah ok).include?(input)
78
+ bot.chat function.call
79
+ unread.tap { line.rewind }
80
+ else
81
+ print "Skipping function call", "\n"
82
+ end
83
+ end
84
+ end
85
+
86
+ def emit
87
+ print formatter(unread).format!(:user), "\n"
88
+ print formatter(unread).format!(:assistant), "\n"
89
+ end
90
+
91
+ def chat(...)
92
+ case options.provider
93
+ when :openai then bot.respond(...)
94
+ else bot.chat(...)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ end unless defined?(LLM)
5
+
6
+ class LLM::Shell
7
+ VERSION = "0.1.0"
8
+ end
data/lib/llm/shell.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "readline"
5
+ require "yaml"
6
+ require "llm"
7
+ require "paint"
8
+
9
+ class LLM::Shell
10
+ require_relative "../io/line"
11
+ require_relative "shell/markdown"
12
+ require_relative "shell/formatter"
13
+ require_relative "shell/default"
14
+ require_relative "shell/options"
15
+ require_relative "shell/repl"
16
+ require_relative "shell/config"
17
+ require_relative "shell/version"
18
+
19
+ ##
20
+ # @return [String]
21
+ def self.home
22
+ File.join Dir.home, ".llm-shell"
23
+ end
24
+
25
+ ##
26
+ # @return [Array<String>]
27
+ def self.tools
28
+ Dir[File.join(home, "tools", "*.rb")]
29
+ end
30
+
31
+ ##
32
+ # @param [Hash] options
33
+ # @return [LLM::Shell]
34
+ def initialize(options)
35
+ @config = Config.new(options[:provider])
36
+ @options = Options.new @config.merge(options), Default.new(options[:provider])
37
+ @bot = LLM::Chat.new(llm, {tools:}.merge(@options.chat)).lazy
38
+ @repl = REPL.new(@bot, options: @options)
39
+ end
40
+
41
+ ##
42
+ # Start the shell
43
+ # @return [void]
44
+ def start
45
+ repl.setup
46
+ repl.start
47
+ end
48
+
49
+ private
50
+
51
+ def tools
52
+ LLM::Shell.tools.filter_map do |path|
53
+ name = File.basename(path, File.extname(path))
54
+ if options.tools.include?(name)
55
+ print Paint["llm-shell: ", :green], "load #{name} tool", "\n"
56
+ eval File.read(path), TOPLEVEL_BINDING, path, 1
57
+ else
58
+ print Paint["llm-shell:: ", :yellow], "skip #{name} tool", "\n"
59
+ end
60
+ end.grep(LLM::Function)
61
+ end
62
+
63
+ attr_reader :options, :bot, :repl
64
+ def provider = LLM.method(options.provider)
65
+ def llm = provider.call(**options.llm)
66
+ end
data/lib/llm-shell.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative "llm/shell"
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../lib/llm/shell"
5
+
6
+ def main(argv)
7
+ options = {tools: []}
8
+ option_parser.parse(argv, into: options)
9
+ if argv.empty? || options[:provider].nil?
10
+ warn option_parser.help
11
+ throw(:exit, 1)
12
+ else
13
+ LLM::Shell.new(options).start
14
+ end
15
+ end
16
+
17
+ def option_parser
18
+ OptionParser.new do |o|
19
+ o.banner = "Usage: llm-shell [OPTIONS]"
20
+ o.on("-p PROVIDER", "--provider NAME", "Required. Options: gemini, openai, anthropic, or ollama.", String)
21
+ o.on("-k [KEY]", "--key [KEY]", "Optional. Required by gemini, openai, and anthropic.", String)
22
+ o.on("-m [MODEL]", "--model [MODEL]", "Optional. The name of a model.", Array)
23
+ o.on("-h [HOST]", "--host [HOST]", "Optional. Sometimes required by ollama.", String)
24
+ o.on("-o [PORT]", "--port [PORT]", "Optional. Sometimes required by ollama.", Integer)
25
+ o.on("-f [GLOB]", "--files [GLOB]", "Optional. Glob pattern(s) separated by a comma.", Array)
26
+ o.on("-t [TOOLS]", "--tools [TOOLS]", "Optional. One or more tool names to load automatically.", Array)
27
+ end
28
+ end
29
+
30
+ excode = catch(:exit) {
31
+ main(ARGV)
32
+ 0
33
+ }
34
+ exit excode
metadata ADDED
@@ -0,0 +1,245 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: llm-shell
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Antar Azri
8
+ - '0x1eef'
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2025-05-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: llm.rb
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.6'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.6'
28
+ - !ruby/object:Gem::Dependency
29
+ name: paint
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.1'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.1'
42
+ - !ruby/object:Gem::Dependency
43
+ name: kramdown
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.5'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.5'
56
+ - !ruby/object:Gem::Dependency
57
+ name: webmock
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: 3.24.0
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 3.24.0
70
+ - !ruby/object:Gem::Dependency
71
+ name: yard
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: 0.9.37
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 0.9.37
84
+ - !ruby/object:Gem::Dependency
85
+ name: kramdown
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '2.4'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '2.4'
98
+ - !ruby/object:Gem::Dependency
99
+ name: webrick
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '1.8'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '1.8'
112
+ - !ruby/object:Gem::Dependency
113
+ name: test-cmd.rb
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: 0.12.0
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: 0.12.0
126
+ - !ruby/object:Gem::Dependency
127
+ name: rake
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '13.0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '13.0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rspec
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '3.0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: '3.0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: standard
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '1.40'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '1.40'
168
+ - !ruby/object:Gem::Dependency
169
+ name: vcr
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: '6.0'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - "~>"
180
+ - !ruby/object:Gem::Version
181
+ version: '6.0'
182
+ - !ruby/object:Gem::Dependency
183
+ name: dotenv
184
+ requirement: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - "~>"
187
+ - !ruby/object:Gem::Version
188
+ version: '2.8'
189
+ type: :development
190
+ prerelease: false
191
+ version_requirements: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - "~>"
194
+ - !ruby/object:Gem::Version
195
+ version: '2.8'
196
+ description: llm-shell is an extensible, developer-oriented command-line utility that
197
+ can interact with multiple Large Language Models (LLMs).
198
+ email:
199
+ - azantar@proton.me
200
+ - 0x1eef@proton.me
201
+ executables:
202
+ - llm-shell
203
+ extensions: []
204
+ extra_rdoc_files: []
205
+ files:
206
+ - README.md
207
+ - bin/llm-shell
208
+ - lib/io/line.rb
209
+ - lib/llm-shell.rb
210
+ - lib/llm/shell.rb
211
+ - lib/llm/shell/config.rb
212
+ - lib/llm/shell/default.rb
213
+ - lib/llm/shell/formatter.rb
214
+ - lib/llm/shell/markdown.rb
215
+ - lib/llm/shell/options.rb
216
+ - lib/llm/shell/repl.rb
217
+ - lib/llm/shell/version.rb
218
+ - libexec/llm-shell/shell
219
+ homepage: https://github.com/llmrb/llm-shell
220
+ licenses:
221
+ - 0BSD
222
+ metadata:
223
+ homepage_uri: https://github.com/llmrb/llm-shell
224
+ source_code_uri: https://github.com/llmrb/llm-shell
225
+ post_install_message:
226
+ rdoc_options: []
227
+ require_paths:
228
+ - lib
229
+ required_ruby_version: !ruby/object:Gem::Requirement
230
+ requirements:
231
+ - - ">="
232
+ - !ruby/object:Gem::Version
233
+ version: 3.0.0
234
+ required_rubygems_version: !ruby/object:Gem::Requirement
235
+ requirements:
236
+ - - ">="
237
+ - !ruby/object:Gem::Version
238
+ version: '0'
239
+ requirements: []
240
+ rubygems_version: 3.5.23
241
+ signing_key:
242
+ specification_version: 4
243
+ summary: llm-shell is an extensible, developer-oriented command-line utility that
244
+ can interact with multiple Large Language Models (LLMs).
245
+ test_files: []