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 +7 -0
- data/README.md +116 -0
- data/bin/llm-shell +20 -0
- data/lib/io/line.rb +26 -0
- data/lib/llm/shell/config.rb +35 -0
- data/lib/llm/shell/default.rb +31 -0
- data/lib/llm/shell/formatter.rb +53 -0
- data/lib/llm/shell/markdown.rb +60 -0
- data/lib/llm/shell/options.rb +30 -0
- data/lib/llm/shell/repl.rb +98 -0
- data/lib/llm/shell/version.rb +8 -0
- data/lib/llm/shell.rb +66 -0
- data/lib/llm-shell.rb +1 -0
- data/libexec/llm-shell/shell +34 -0
- metadata +245 -0
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
|
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: []
|