llm-shell 0.9.2 → 0.11.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/LICENSE +21 -0
- data/README.md +24 -165
- data/exe/llm-shell +7 -0
- data/llm-shell.gemspec +26 -36
- metadata +32 -198
- data/bin/llm-shell +0 -21
- data/lib/io/line.rb +0 -26
- data/lib/llm/function.rb +0 -17
- data/lib/llm/shell/command/extension.rb +0 -42
- data/lib/llm/shell/command.rb +0 -88
- data/lib/llm/shell/commands/clear_screen.rb +0 -32
- data/lib/llm/shell/commands/dir_import.rb +0 -49
- data/lib/llm/shell/commands/file_import.rb +0 -41
- data/lib/llm/shell/commands/help.rb +0 -66
- data/lib/llm/shell/commands/show_chat.rb +0 -46
- data/lib/llm/shell/commands/show_version.rb +0 -33
- data/lib/llm/shell/commands/system_prompt.rb +0 -35
- data/lib/llm/shell/commands/utils.rb +0 -21
- data/lib/llm/shell/completion.rb +0 -40
- data/lib/llm/shell/config.rb +0 -35
- data/lib/llm/shell/default.rb +0 -23
- data/lib/llm/shell/formatter.rb +0 -42
- data/lib/llm/shell/functions/read_file.rb +0 -22
- data/lib/llm/shell/functions/write_file.rb +0 -22
- data/lib/llm/shell/markdown.rb +0 -83
- data/lib/llm/shell/options.rb +0 -39
- data/lib/llm/shell/renderer.rb +0 -52
- data/lib/llm/shell/repl.rb +0 -106
- data/lib/llm/shell/version.rb +0 -8
- data/lib/llm/shell.rb +0 -88
- data/lib/llm-shell.rb +0 -3
- data/libexec/llm-shell/shell +0 -45
- data/share/llm-shell/prompts/default.txt +0 -21
data/lib/llm/shell/markdown.rb
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "redcarpet"
|
|
4
|
-
require "coderay"
|
|
5
|
-
|
|
6
|
-
class LLM::Shell
|
|
7
|
-
##
|
|
8
|
-
# @api private
|
|
9
|
-
# @see redcarpet https://github.com/vmg/redcarpet/blob/master/ext/redcarpet/markdown.h#L69-L110
|
|
10
|
-
class Markdown < Redcarpet::Render::Base
|
|
11
|
-
##
|
|
12
|
-
# Renders markdown text to a terminal-friendly format.
|
|
13
|
-
# @return [String
|
|
14
|
-
def self.render(text)
|
|
15
|
-
renderer = Redcarpet::Markdown.new(self, options)
|
|
16
|
-
renderer.render(wrap(p: text)).strip
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
##
|
|
20
|
-
# @api private
|
|
21
|
-
def self.wrap(p:, width: 80)
|
|
22
|
-
in_code = false
|
|
23
|
-
p.lines.map do |line|
|
|
24
|
-
if line =~ /^(\s*)(```|~~~)/
|
|
25
|
-
in_code = !in_code
|
|
26
|
-
line
|
|
27
|
-
elsif in_code || line =~ /^\s{4}/
|
|
28
|
-
line
|
|
29
|
-
else
|
|
30
|
-
line.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
|
|
31
|
-
end
|
|
32
|
-
end.join.strip + "\n"
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
##
|
|
36
|
-
# @api private
|
|
37
|
-
def self.options
|
|
38
|
-
{
|
|
39
|
-
autolink: false, no_intra_emphasis: true,
|
|
40
|
-
fenced_code_blocks: true, lax_spacing: true,
|
|
41
|
-
strikethrough: true, superscript: true,
|
|
42
|
-
tables: true, with_toc_data: true
|
|
43
|
-
}
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def block_code(code, lang)
|
|
47
|
-
["\n", Paint["#{lang}:", :blue, :bold],
|
|
48
|
-
"\n", coderay(code, lang),
|
|
49
|
-
"\n"].join
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def header(text, level)
|
|
53
|
-
color = levels.fetch(level, :white)
|
|
54
|
-
"\n" + Paint[("#" * level) + " " + text, color] + "\n"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def paragraph(p) = "#{p.strip}\n\n"
|
|
58
|
-
def list(items, _type) = items
|
|
59
|
-
def list_item(item, _type) = "\n• #{item.strip}\n"
|
|
60
|
-
def emphasis(text) = Paint[text, :italic]
|
|
61
|
-
def double_emphasis(text) = Paint[text, :bold]
|
|
62
|
-
def codespan(code) = Paint[code, :yellow, :underline]
|
|
63
|
-
def block_quote(quote) = Paint[quote, :italic]
|
|
64
|
-
def normal_text(text) = text
|
|
65
|
-
def linebreak = "\n"
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
def coderay(code, lang)
|
|
70
|
-
CodeRay.scan(code, lang).terminal
|
|
71
|
-
rescue ArgumentError
|
|
72
|
-
lang = "text"
|
|
73
|
-
retry
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def levels
|
|
77
|
-
{
|
|
78
|
-
1 => :green, 2 => :blue, 3 => :green,
|
|
79
|
-
4 => :yellow, 5 => :red, 6 => :purple
|
|
80
|
-
}
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
data/lib/llm/shell/options.rb
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
@prompt = @options[:prompt] ? custom_prompt : default.prompt
|
|
19
|
-
@files = Dir[*@options.delete(:files) || []].reject { File.directory?(_1) }
|
|
20
|
-
@bot_options = {model: @options.delete(:model)}.compact
|
|
21
|
-
@default = default
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def provider = @provider
|
|
25
|
-
def tools = @tools
|
|
26
|
-
def files = @files
|
|
27
|
-
def llm = @options
|
|
28
|
-
def bot = @bot_options
|
|
29
|
-
def default = @default
|
|
30
|
-
def prompt = File.read(@prompt)
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def custom_prompt
|
|
35
|
-
prompt = @options.delete(:prompt)
|
|
36
|
-
File.join(LLM::Shell.home, "prompts", prompt)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
data/lib/llm/shell/renderer.rb
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class LLM::Shell
|
|
4
|
-
class Renderer
|
|
5
|
-
RenderError = Class.new(RuntimeError)
|
|
6
|
-
include Command::Utils
|
|
7
|
-
|
|
8
|
-
##
|
|
9
|
-
# @param [LLM::Message] message
|
|
10
|
-
# The message to render
|
|
11
|
-
# @return [LLM::Shell::MessageRenderer]
|
|
12
|
-
# Returns an instance of the renderer
|
|
13
|
-
def initialize(message)
|
|
14
|
-
@message = message
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def render
|
|
18
|
-
if message.user?
|
|
19
|
-
render_message(message, :yellow)
|
|
20
|
-
elsif message.assistant?
|
|
21
|
-
render_message(message, :green)
|
|
22
|
-
elsif message.system?
|
|
23
|
-
render_message(message, :red)
|
|
24
|
-
else
|
|
25
|
-
raise RenderError.new("no handler for message role '#{message.role}'")
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
attr_reader :message
|
|
32
|
-
|
|
33
|
-
def render_message(message, color)
|
|
34
|
-
role = Paint[message.role, :bold, color]
|
|
35
|
-
title = "#{role} says: "
|
|
36
|
-
content = message.content
|
|
37
|
-
if message.tool_call?
|
|
38
|
-
body = "Tool call(s) request"
|
|
39
|
-
elsif message.tool_return?
|
|
40
|
-
body = "Tool call(s) return"
|
|
41
|
-
elsif Array === content
|
|
42
|
-
body = content.map { |c| render_message(c, color) }.join("\n")
|
|
43
|
-
elsif content =~ file_pattern
|
|
44
|
-
path = content.match(file_pattern) ? Regexp.last_match[1] : nil
|
|
45
|
-
body = "<file path=#{path} />"
|
|
46
|
-
else
|
|
47
|
-
body = Markdown.render(content)
|
|
48
|
-
end
|
|
49
|
-
[title, "\n", body, "\n"].join
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
data/lib/llm/shell/repl.rb
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
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
|
-
@io = IO::Line.new($stdout)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
##
|
|
21
|
-
# Performs initial setup
|
|
22
|
-
# @return [void]
|
|
23
|
-
def setup
|
|
24
|
-
LLM::Shell.commands.each { |file| require file }
|
|
25
|
-
Readline.completion_proc = Completion.to_proc
|
|
26
|
-
chat options.prompt, role: options.default.role
|
|
27
|
-
files.each { chat ["--- START: #{_1} ---", File.read(_1), "--- END: #{_1} ---"].join("\n") }
|
|
28
|
-
bot.messages.each(&:read!)
|
|
29
|
-
clear_screen
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
##
|
|
33
|
-
# Enters the main loop
|
|
34
|
-
# @return [void]
|
|
35
|
-
def start
|
|
36
|
-
loop do
|
|
37
|
-
read
|
|
38
|
-
eval while functions.any?
|
|
39
|
-
emit
|
|
40
|
-
rescue LLM::ResponseError => ex
|
|
41
|
-
print Paint[ex.response.class, :red], "\n"
|
|
42
|
-
print ex.response.body, "\n"
|
|
43
|
-
rescue => ex
|
|
44
|
-
print Paint[ex.class, :red], "\n"
|
|
45
|
-
print ex.message, "\n"
|
|
46
|
-
print ex.backtrace[0..5].join("\n")
|
|
47
|
-
rescue Interrupt
|
|
48
|
-
throw(:exit, 0)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def read
|
|
55
|
-
input = Readline.readline("llm> ", true) || throw(:exit, 0)
|
|
56
|
-
words = input.split(" ")
|
|
57
|
-
if LLM.commands[words[0]]
|
|
58
|
-
cmd = LLM.commands[words[0]]
|
|
59
|
-
argv = words[1..]
|
|
60
|
-
cmd.setup(bot, io)
|
|
61
|
-
cmd.call(*argv)
|
|
62
|
-
else
|
|
63
|
-
chat input.tap { clear_screen }
|
|
64
|
-
io.rewind.print(Paint["Thinking", :bold])
|
|
65
|
-
unread.tap { io.rewind }
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def eval
|
|
70
|
-
callables = []
|
|
71
|
-
cancels = []
|
|
72
|
-
results = []
|
|
73
|
-
functions.each do |function|
|
|
74
|
-
print Paint["system", :bold, :red], " says: ", "\n"
|
|
75
|
-
print "function: ", function.name, "\n"
|
|
76
|
-
print "arguments: ", function.arguments.to_h.size, "\n"
|
|
77
|
-
input = Readline.readline("Do you want to call it ? ", true)
|
|
78
|
-
puts
|
|
79
|
-
if %w(y yes yep yeah ok).include?(input)
|
|
80
|
-
callables << function
|
|
81
|
-
else
|
|
82
|
-
cancels << function
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
results.concat callables.map(&:call)
|
|
86
|
-
results.concat cancels.map(&:cancel)
|
|
87
|
-
bot.chat(results)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def emit
|
|
91
|
-
LLM::Shell.pager do |io|
|
|
92
|
-
io.write formatter(unread).format!(:user), "\n"
|
|
93
|
-
io.write formatter(unread).format!(:assistant), "\n"
|
|
94
|
-
end unless unread.empty?
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
attr_reader :bot, :console, :io, :default, :options
|
|
98
|
-
|
|
99
|
-
def formatter(messages) = Formatter.new(messages)
|
|
100
|
-
def unread = bot.messages.unread
|
|
101
|
-
def functions = bot.functions
|
|
102
|
-
def files = @options.files
|
|
103
|
-
def clear_screen = console.clear_screen
|
|
104
|
-
def chat(...) = bot.chat(...)
|
|
105
|
-
end
|
|
106
|
-
end
|
data/lib/llm/shell/version.rb
DELETED
data/lib/llm/shell.rb
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
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 "function"
|
|
11
|
-
require_relative "../io/line"
|
|
12
|
-
require_relative "shell/command"
|
|
13
|
-
require_relative "shell/command/extension"
|
|
14
|
-
require_relative "shell/markdown"
|
|
15
|
-
require_relative "shell/renderer"
|
|
16
|
-
require_relative "shell/formatter"
|
|
17
|
-
require_relative "shell/default"
|
|
18
|
-
require_relative "shell/options"
|
|
19
|
-
require_relative "shell/repl"
|
|
20
|
-
require_relative "shell/config"
|
|
21
|
-
require_relative "shell/completion"
|
|
22
|
-
require_relative "shell/version"
|
|
23
|
-
|
|
24
|
-
##
|
|
25
|
-
# Load all commands
|
|
26
|
-
Dir[File.join(__dir__, "shell", "commands", "*.rb")].each { require(_1) }
|
|
27
|
-
|
|
28
|
-
##
|
|
29
|
-
# Opens a pager
|
|
30
|
-
# @return [void]
|
|
31
|
-
def self.pager(...)
|
|
32
|
-
IO.popen("less -FRX", "w", ...)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
##
|
|
36
|
-
# @return [String]
|
|
37
|
-
def self.home
|
|
38
|
-
File.join Dir.home, ".llm-shell"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
##
|
|
42
|
-
# @return [Array<String>]
|
|
43
|
-
def self.tools
|
|
44
|
-
Dir[*TOOLGLOBS]
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
##
|
|
48
|
-
# @return [Array<String>]
|
|
49
|
-
def self.commands
|
|
50
|
-
Dir[File.join(home, "commands", "*.rb")]
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
TOOLGLOBS = [
|
|
54
|
-
File.join(home, "tools", "*.rb"),
|
|
55
|
-
File.join(__dir__, "shell", "functions", "*.rb")
|
|
56
|
-
].freeze
|
|
57
|
-
private_constant :TOOLGLOBS
|
|
58
|
-
|
|
59
|
-
##
|
|
60
|
-
# @param [Hash] options
|
|
61
|
-
# @return [LLM::Shell]
|
|
62
|
-
def initialize(options)
|
|
63
|
-
@config = Config.new(options[:provider])
|
|
64
|
-
@options = Options.new @config.merge(options), Default.new(options[:provider])
|
|
65
|
-
@bot = LLM::Bot.new(llm, {tools:}.merge(@options.bot))
|
|
66
|
-
@repl = REPL.new(bot: @bot, options: @options)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
##
|
|
70
|
-
# Start the shell
|
|
71
|
-
# @return [void]
|
|
72
|
-
def start
|
|
73
|
-
repl.setup
|
|
74
|
-
repl.start
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
private
|
|
78
|
-
|
|
79
|
-
def tools
|
|
80
|
-
LLM::Shell.tools.map do |path|
|
|
81
|
-
eval File.read(path), TOPLEVEL_BINDING, path, 1
|
|
82
|
-
end.grep(LLM::Function)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
attr_reader :options, :bot, :repl
|
|
86
|
-
def provider = LLM.method(options.provider)
|
|
87
|
-
def llm = provider.call(**options.llm)
|
|
88
|
-
end
|
data/lib/llm-shell.rb
DELETED
data/libexec/llm-shell/shell
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require_relative "../../lib/llm/shell"
|
|
5
|
-
|
|
6
|
-
def main(argv)
|
|
7
|
-
if argv.include?("-v") || argv.include?("--version")
|
|
8
|
-
puts LLM::Shell::VERSION
|
|
9
|
-
else
|
|
10
|
-
options = {tools: []}
|
|
11
|
-
option_parser.parse(argv, into: options)
|
|
12
|
-
if argv.empty? || options[:provider].nil?
|
|
13
|
-
warn option_parser.help
|
|
14
|
-
throw(:exit, 1)
|
|
15
|
-
else
|
|
16
|
-
LLM::Shell.new(options).start
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def option_parser
|
|
22
|
-
OptionParser.new do |o|
|
|
23
|
-
o.banner = "Usage: llm-shell [OPTIONS]"
|
|
24
|
-
o.on("-p PROVIDER", "--provider NAME", "Required. Options: gemini, openai, anthropic, ollama or llamacpp.", String)
|
|
25
|
-
o.on("-k [KEY]", "--key [KEY]", "Optional. Required by gemini, openai, and anthropic.", String)
|
|
26
|
-
o.on("-m [MODEL]", "--model [MODEL]", "Optional. The name of a model.", Array)
|
|
27
|
-
o.on("-h [HOST]", "--host [HOST]", "Optional. Sometimes required by ollama.", String)
|
|
28
|
-
o.on("-o [PORT]", "--port [PORT]", "Optional. Sometimes required by ollama.", Integer)
|
|
29
|
-
o.on("-f [GLOB]", "--files [GLOB]", "Optional. Glob pattern(s) separated by a comma.", Array)
|
|
30
|
-
o.on("-r [PROMPT]", "--prompt [PROMPT]", "Optional. The prompt to use.", String)
|
|
31
|
-
o.on("-v", "--version", "Optional. Print the version and exit.")
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
excode = catch(:exit) do
|
|
36
|
-
main(ARGV)
|
|
37
|
-
0
|
|
38
|
-
rescue => ex
|
|
39
|
-
print Paint[ex.class, :red, :bold], "\n"
|
|
40
|
-
print ex.message, "\n\n"
|
|
41
|
-
print Paint["Backtrace", :bold], "\n"
|
|
42
|
-
print ex.backtrace[0..5].join("\n"), "\n"
|
|
43
|
-
1
|
|
44
|
-
end
|
|
45
|
-
exit excode
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
## Task
|
|
2
|
-
|
|
3
|
-
Your task is to provide helpful assistance to the user.
|
|
4
|
-
|
|
5
|
-
## Context
|
|
6
|
-
|
|
7
|
-
The user is talking to you from a terminal-based application.
|
|
8
|
-
|
|
9
|
-
## Inputs
|
|
10
|
-
|
|
11
|
-
The user might send you the content of file(s) from their filesystem.
|
|
12
|
-
When the user sends you a file, it will be in this format:
|
|
13
|
-
|
|
14
|
-
<file path="/path/to/file">
|
|
15
|
-
CONTENT
|
|
16
|
-
</file>
|
|
17
|
-
|
|
18
|
-
## Outputs
|
|
19
|
-
|
|
20
|
-
Always respond in markdown.
|
|
21
|
-
When the user sends a file, reply with "Got it" and nothing else.
|