elelem 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/exe/elelem +3 -0
- data/lib/elelem/agent.rb +14 -1
- data/lib/elelem/api.rb +2 -4
- data/lib/elelem/application.rb +21 -21
- data/lib/elelem/configuration.rb +21 -2
- data/lib/elelem/conversation.rb +9 -10
- data/lib/elelem/mcp_client.rb +99 -0
- data/lib/elelem/state.rb +64 -13
- data/lib/elelem/system_prompt.erb +7 -0
- data/lib/elelem/tool.rb +116 -0
- data/lib/elelem/tools.rb +13 -42
- data/lib/elelem/tui.rb +41 -2
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +5 -0
- metadata +47 -5
- data/.rspec +0 -3
- data/.rubocop.yml +0 -8
- data/mise.toml +0 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a5469d2d253c6e0d09f60de69dafffdffb305da3f5864a4f6aa9bbac5ae7da6
|
4
|
+
data.tar.gz: 60d9d35b759e0722b557cefbb4fb6d6b1713661de432efb8283ae1cd7f951e05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46073936ca9abcf83897e83355b7f979ebf2866d16351ccced55d70a8c311d8068acb3a78126b85b7b6622b45e6b2d51dc86f35914a25fdec937ad60214e4da9
|
7
|
+
data.tar.gz: d7233c12ed3b2359c5c7a1d47e908951e0651f19fb8d07777e34aa0fb5ddfc51f4368a7a192136a743d1009a9a26239da572555dd5d3d90e99e1a4e2668c2d47
|
data/CHANGELOG.md
CHANGED
data/exe/elelem
CHANGED
data/lib/elelem/agent.rb
CHANGED
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Agent
|
5
|
-
attr_reader :api, :conversation, :logger
|
5
|
+
attr_reader :api, :conversation, :logger, :model
|
6
6
|
|
7
7
|
def initialize(configuration)
|
8
8
|
@api = configuration.api
|
9
9
|
@configuration = configuration
|
10
|
+
@model = configuration.model
|
10
11
|
@conversation = configuration.conversation
|
11
12
|
@logger = configuration.logger
|
12
13
|
transition_to(Idle.new)
|
@@ -36,6 +37,18 @@ module Elelem
|
|
36
37
|
configuration.tools.execute(tool_call)
|
37
38
|
end
|
38
39
|
|
40
|
+
def show_progress(message, prefix = "[.]", colour: :gray)
|
41
|
+
configuration.tui.show_progress(message, prefix, colour: colour)
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear_line
|
45
|
+
configuration.tui.clear_line
|
46
|
+
end
|
47
|
+
|
48
|
+
def complete_progress(message = "Completed")
|
49
|
+
configuration.tui.complete_progress(message)
|
50
|
+
end
|
51
|
+
|
39
52
|
def quit
|
40
53
|
logger.debug("Exiting...")
|
41
54
|
exit
|
data/lib/elelem/api.rb
CHANGED
@@ -8,7 +8,7 @@ module Elelem
|
|
8
8
|
@configuration = configuration
|
9
9
|
end
|
10
10
|
|
11
|
-
def chat(messages)
|
11
|
+
def chat(messages, &block)
|
12
12
|
body = {
|
13
13
|
messages: messages,
|
14
14
|
model: configuration.model,
|
@@ -28,9 +28,7 @@ module Elelem
|
|
28
28
|
configuration.http.request(req) do |response|
|
29
29
|
raise response.inspect unless response.code == "200"
|
30
30
|
|
31
|
-
response.read_body
|
32
|
-
yield(chunk)
|
33
|
-
end
|
31
|
+
response.read_body(&block)
|
34
32
|
end
|
35
33
|
end
|
36
34
|
end
|
data/lib/elelem/application.rb
CHANGED
@@ -4,29 +4,29 @@ module Elelem
|
|
4
4
|
class Application < Thor
|
5
5
|
desc "chat", "Start the REPL"
|
6
6
|
method_option :help,
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
aliases: "-h",
|
8
|
+
type: :boolean,
|
9
|
+
desc: "Display usage information"
|
10
10
|
method_option :host,
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
aliases: "--host",
|
12
|
+
type: :string,
|
13
|
+
desc: "Ollama host",
|
14
|
+
default: ENV.fetch("OLLAMA_HOST", "localhost:11434")
|
15
15
|
method_option :model,
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
aliases: "--model",
|
17
|
+
type: :string,
|
18
|
+
desc: "Ollama model",
|
19
|
+
default: ENV.fetch("OLLAMA_MODEL", "gpt-oss")
|
20
20
|
method_option :token,
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
aliases: "--token",
|
22
|
+
type: :string,
|
23
|
+
desc: "Ollama token",
|
24
|
+
default: ENV.fetch("OLLAMA_API_KEY", nil)
|
25
25
|
method_option :debug,
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
aliases: "--debug",
|
27
|
+
type: :boolean,
|
28
|
+
desc: "Debug mode",
|
29
|
+
default: false
|
30
30
|
def chat(*)
|
31
31
|
if options[:help]
|
32
32
|
invoke :help, ["chat"]
|
@@ -37,8 +37,8 @@ module Elelem
|
|
37
37
|
token: options[:token],
|
38
38
|
debug: options[:debug]
|
39
39
|
)
|
40
|
-
say "
|
41
|
-
say
|
40
|
+
say "Agent (#{configuration.model})", :green
|
41
|
+
say configuration.tools.banner.to_s, :green
|
42
42
|
|
43
43
|
agent = Agent.new(configuration)
|
44
44
|
agent.repl
|
data/lib/elelem/configuration.rb
CHANGED
@@ -28,7 +28,7 @@ module Elelem
|
|
28
28
|
|
29
29
|
def logger
|
30
30
|
@logger ||= Logger.new(debug ? "elelem.log" : "/dev/null").tap do |logger|
|
31
|
-
logger.formatter = ->(_, _, _, message) { message.strip
|
31
|
+
logger.formatter = ->(_, _, _, message) { "#{message.to_s.strip}\n" }
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -41,7 +41,7 @@ module Elelem
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def tools
|
44
|
-
@tools ||= Tools.new
|
44
|
+
@tools ||= Tools.new(self, [BashTool.new(self)] + mcp_tools)
|
45
45
|
end
|
46
46
|
|
47
47
|
private
|
@@ -49,5 +49,24 @@ module Elelem
|
|
49
49
|
def scheme
|
50
50
|
host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
|
51
51
|
end
|
52
|
+
|
53
|
+
def mcp_tools(clients = [serena_client])
|
54
|
+
return [] if ENV["SMALL"]
|
55
|
+
|
56
|
+
@mcp_tools ||= clients.map { |client| client.tools.map { |tool| MCPTool.new(client, tui, tool) } }.flatten
|
57
|
+
end
|
58
|
+
|
59
|
+
def serena_client
|
60
|
+
MCPClient.new(self, [
|
61
|
+
"uvx",
|
62
|
+
"--from",
|
63
|
+
"git+https://github.com/oraios/serena",
|
64
|
+
"serena",
|
65
|
+
"start-mcp-server",
|
66
|
+
"--transport", "stdio",
|
67
|
+
"--context", "ide-assistant",
|
68
|
+
"--project", Dir.pwd
|
69
|
+
])
|
70
|
+
end
|
52
71
|
end
|
53
72
|
end
|
data/lib/elelem/conversation.rb
CHANGED
@@ -2,16 +2,9 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Conversation
|
5
|
-
|
6
|
-
You are ChatGPT, a helpful assistant with reasoning capabilities.
|
7
|
-
Current date: #{Time.now.strftime("%Y-%m-%d")}.
|
8
|
-
System info: `uname -a` output: #{`uname -a`.strip}
|
9
|
-
Reasoning: high
|
10
|
-
SYS
|
5
|
+
ROLES = %i[system assistant user tool].freeze
|
11
6
|
|
12
|
-
|
13
|
-
|
14
|
-
def initialize(items = [{ role: "system", content: SYSTEM_MESSAGE }])
|
7
|
+
def initialize(items = [{ role: "system", content: system_prompt }])
|
15
8
|
@items = items
|
16
9
|
end
|
17
10
|
|
@@ -23,7 +16,7 @@ module Elelem
|
|
23
16
|
def add(role: :user, content: "")
|
24
17
|
role = role.to_sym
|
25
18
|
raise "unknown role: #{role}" unless ROLES.include?(role)
|
26
|
-
return if content.empty?
|
19
|
+
return if content.nil? || content.empty?
|
27
20
|
|
28
21
|
if @items.last && @items.last[:role] == role
|
29
22
|
@items.last[:content] += content
|
@@ -31,5 +24,11 @@ module Elelem
|
|
31
24
|
@items.push({ role: role, content: content })
|
32
25
|
end
|
33
26
|
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def system_prompt
|
31
|
+
ERB.new(Pathname.new(__dir__).join("system_prompt.erb").read).result(binding)
|
32
|
+
end
|
34
33
|
end
|
35
34
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class MCPClient
|
5
|
+
attr_reader :tools
|
6
|
+
|
7
|
+
def initialize(configuration, command = [])
|
8
|
+
@configuration = configuration
|
9
|
+
@stdin, @stdout, @stderr, @worker = Open3.popen3(*command, pgroup: true)
|
10
|
+
|
11
|
+
# 1. Send initialize request
|
12
|
+
send_request(
|
13
|
+
method: "initialize",
|
14
|
+
params: {
|
15
|
+
protocolVersion: "2024-11-05",
|
16
|
+
capabilities: {
|
17
|
+
tools: {}
|
18
|
+
},
|
19
|
+
clientInfo: {
|
20
|
+
name: "Elelem",
|
21
|
+
version: Elelem::VERSION
|
22
|
+
}
|
23
|
+
}
|
24
|
+
)
|
25
|
+
|
26
|
+
# 2. Send initialized notification (required by MCP protocol)
|
27
|
+
send_notification(method: "notifications/initialized")
|
28
|
+
|
29
|
+
# 3. Now we can request tools
|
30
|
+
@tools = send_request(method: "tools/list")&.dig("tools") || []
|
31
|
+
end
|
32
|
+
|
33
|
+
def connected?
|
34
|
+
return false unless @worker&.alive?
|
35
|
+
return false unless @stdin && !@stdin.closed?
|
36
|
+
return false unless @stdout && !@stdout.closed?
|
37
|
+
|
38
|
+
begin
|
39
|
+
Process.getpgid(@worker.pid)
|
40
|
+
true
|
41
|
+
rescue Errno::ESRCH
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def call(name, arguments = {})
|
47
|
+
send_request(
|
48
|
+
method: "tools/call",
|
49
|
+
params: {
|
50
|
+
name: name,
|
51
|
+
arguments: arguments
|
52
|
+
}
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
attr_reader :stdin, :stdout, :stderr, :worker, :configuration
|
59
|
+
|
60
|
+
def send_request(method:, params: {})
|
61
|
+
return {} unless connected?
|
62
|
+
|
63
|
+
request = {
|
64
|
+
jsonrpc: "2.0",
|
65
|
+
id: Time.now.to_i,
|
66
|
+
method: method
|
67
|
+
}
|
68
|
+
request[:params] = params unless params.empty?
|
69
|
+
configuration.logger.debug(JSON.pretty_generate(request))
|
70
|
+
|
71
|
+
@stdin.puts(JSON.generate(request))
|
72
|
+
@stdin.flush
|
73
|
+
|
74
|
+
response_line = @stdout.gets&.strip
|
75
|
+
return {} if response_line.nil? || response_line.empty?
|
76
|
+
|
77
|
+
response = JSON.parse(response_line)
|
78
|
+
configuration.logger.debug(JSON.pretty_generate(response))
|
79
|
+
|
80
|
+
if response["error"]
|
81
|
+
configuration.logger.error(response["error"]["message"])
|
82
|
+
{ error: response["error"]["message"] }
|
83
|
+
else
|
84
|
+
response["result"]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def send_notification(method:, params: {})
|
89
|
+
notification = {
|
90
|
+
jsonrpc: "2.0",
|
91
|
+
method: method
|
92
|
+
}
|
93
|
+
notification[:params] = params unless params.empty?
|
94
|
+
configuration.logger.debug("Sending notification: #{JSON.pretty_generate(notification)}")
|
95
|
+
@stdin.puts(JSON.generate(notification))
|
96
|
+
@stdin.flush
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/elelem/state.rb
CHANGED
@@ -4,12 +4,19 @@ module Elelem
|
|
4
4
|
class Idle
|
5
5
|
def run(agent)
|
6
6
|
agent.logger.debug("Idling...")
|
7
|
-
|
8
|
-
|
7
|
+
agent.say("#{Dir.pwd} (#{agent.model}) [#{git_branch}]", colour: :magenta, newline: true)
|
8
|
+
input = agent.prompt("モ ")
|
9
|
+
agent.quit if input.nil? || input.empty? || input == "exit" || input == "quit"
|
9
10
|
|
10
11
|
agent.conversation.add(role: :user, content: input)
|
11
12
|
agent.transition_to(Working.new)
|
12
13
|
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def git_branch
|
18
|
+
`git branch --no-color --show-current --no-abbrev`.strip
|
19
|
+
end
|
13
20
|
end
|
14
21
|
|
15
22
|
class Working
|
@@ -27,29 +34,39 @@ module Elelem
|
|
27
34
|
|
28
35
|
class Waiting < State
|
29
36
|
def process(message)
|
30
|
-
|
37
|
+
state_for(message)&.process(message)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
31
41
|
|
42
|
+
def state_for(message)
|
32
43
|
if message["thinking"] && !message["thinking"].empty?
|
33
|
-
|
44
|
+
Thinking.new(agent)
|
34
45
|
elsif message["tool_calls"]&.any?
|
35
|
-
|
46
|
+
Executing.new(agent)
|
36
47
|
elsif message["content"] && !message["content"].empty?
|
37
|
-
|
38
|
-
else
|
39
|
-
state = nil
|
48
|
+
Talking.new(agent)
|
40
49
|
end
|
41
|
-
|
42
|
-
state&.process(message)
|
43
50
|
end
|
44
51
|
end
|
45
52
|
|
46
53
|
class Thinking < State
|
54
|
+
def initialize(agent)
|
55
|
+
super(agent)
|
56
|
+
@progress_shown = false
|
57
|
+
end
|
58
|
+
|
47
59
|
def process(message)
|
48
60
|
if message["thinking"] && !message["thinking"]&.empty?
|
61
|
+
unless @progress_shown
|
62
|
+
agent.show_progress("Thinking...", "[*]", colour: :yellow)
|
63
|
+
agent.say("\n\n", newline: false)
|
64
|
+
@progress_shown = true
|
65
|
+
end
|
49
66
|
agent.say(message["thinking"], colour: :gray, newline: false)
|
50
67
|
self
|
51
68
|
else
|
52
|
-
agent.say("", newline:
|
69
|
+
agent.say("\n\n", newline: false)
|
53
70
|
Waiting.new(agent).process(message)
|
54
71
|
end
|
55
72
|
end
|
@@ -59,7 +76,15 @@ module Elelem
|
|
59
76
|
def process(message)
|
60
77
|
if message["tool_calls"]&.any?
|
61
78
|
message["tool_calls"].each do |tool_call|
|
62
|
-
|
79
|
+
tool_name = tool_call.dig("function", "name") || "unknown"
|
80
|
+
agent.show_progress(tool_name, "[>]", colour: :magenta)
|
81
|
+
agent.say("\n\n", newline: false)
|
82
|
+
|
83
|
+
output = agent.execute(tool_call)
|
84
|
+
agent.conversation.add(role: :tool, content: output)
|
85
|
+
|
86
|
+
agent.say("\n", newline: false)
|
87
|
+
agent.complete_progress("#{tool_name} completed")
|
63
88
|
end
|
64
89
|
end
|
65
90
|
|
@@ -67,14 +92,37 @@ module Elelem
|
|
67
92
|
end
|
68
93
|
end
|
69
94
|
|
95
|
+
class Error < State
|
96
|
+
def initialize(agent, error_message)
|
97
|
+
super(agent)
|
98
|
+
@error_message = error_message
|
99
|
+
end
|
100
|
+
|
101
|
+
def process(_message)
|
102
|
+
agent.say("\nTool execution failed: #{@error_message}", colour: :red)
|
103
|
+
agent.say("Returning to idle state.\n\n", colour: :yellow)
|
104
|
+
Waiting.new(agent)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
70
108
|
class Talking < State
|
109
|
+
def initialize(agent)
|
110
|
+
super(agent)
|
111
|
+
@progress_shown = false
|
112
|
+
end
|
113
|
+
|
71
114
|
def process(message)
|
72
115
|
if message["content"] && !message["content"]&.empty?
|
116
|
+
unless @progress_shown
|
117
|
+
agent.show_progress("Responding...", "[~]", colour: :white)
|
118
|
+
agent.say("\n", newline: false)
|
119
|
+
@progress_shown = true
|
120
|
+
end
|
73
121
|
agent.conversation.add(role: message["role"], content: message["content"])
|
74
122
|
agent.say(message["content"], colour: :default, newline: false)
|
75
123
|
self
|
76
124
|
else
|
77
|
-
agent.say("", newline:
|
125
|
+
agent.say("\n\n", newline: false)
|
78
126
|
Waiting.new(agent).process(message)
|
79
127
|
end
|
80
128
|
end
|
@@ -82,6 +130,9 @@ module Elelem
|
|
82
130
|
|
83
131
|
def run(agent)
|
84
132
|
agent.logger.debug("Working...")
|
133
|
+
agent.show_progress("Processing...", "[.]", colour: :cyan)
|
134
|
+
agent.say("\n\n", newline: false)
|
135
|
+
|
85
136
|
state = Waiting.new(agent)
|
86
137
|
done = false
|
87
138
|
|
@@ -0,0 +1,7 @@
|
|
1
|
+
**Del — AI** — Direct/no fluff; prose unless bullets; concise/simple, thorough/complex; critical>agree; honest always; AI≠human. TDD→SOLID→SRP/encapsulation/composition>inheritance; patterns only if needed; self-doc names; simple>complex; no cleverness. Unix: small tools, 1 job, pipe; prefer built-ins; cite man(1); note POSIX≠GNU; stdin/stdout streams.
|
2
|
+
|
3
|
+
Time: `<%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %>`
|
4
|
+
Project Directory: `<%= Dir.pwd %>`
|
5
|
+
System Info: `<%= `uname -a`.strip %>`
|
6
|
+
|
7
|
+
Del is now being connected with a person.
|
data/lib/elelem/tool.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class Tool
|
5
|
+
attr_reader :name, :description, :parameters
|
6
|
+
|
7
|
+
def initialize(name, description, parameters)
|
8
|
+
@name = name
|
9
|
+
@description = description
|
10
|
+
@parameters = parameters
|
11
|
+
end
|
12
|
+
|
13
|
+
def banner
|
14
|
+
[name, parameters].join(": ")
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?(args)
|
18
|
+
JSON::Validator.validate(parameters, args, insert_defaults: true)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
{
|
23
|
+
type: "function",
|
24
|
+
function: {
|
25
|
+
name: name,
|
26
|
+
description: description,
|
27
|
+
parameters: parameters
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class BashTool < Tool
|
34
|
+
attr_reader :tui
|
35
|
+
|
36
|
+
def initialize(configuration)
|
37
|
+
@tui = configuration.tui
|
38
|
+
super("bash", "Execute a shell command.", {
|
39
|
+
type: "object",
|
40
|
+
properties: {
|
41
|
+
command: { type: "string" }
|
42
|
+
},
|
43
|
+
required: ["command"]
|
44
|
+
})
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(args)
|
48
|
+
command = args["command"]
|
49
|
+
output_buffer = []
|
50
|
+
|
51
|
+
Open3.popen3("/bin/sh", "-c", command) do |stdin, stdout, stderr, wait_thread|
|
52
|
+
stdin.close
|
53
|
+
streams = [stdout, stderr]
|
54
|
+
|
55
|
+
until streams.empty?
|
56
|
+
ready = IO.select(streams, nil, nil, 0.1)
|
57
|
+
|
58
|
+
if ready
|
59
|
+
ready[0].each do |io|
|
60
|
+
data = io.read_nonblock(4096)
|
61
|
+
output_buffer << data
|
62
|
+
|
63
|
+
if io == stderr
|
64
|
+
tui.say(data, colour: :red, newline: false)
|
65
|
+
else
|
66
|
+
tui.say(data, newline: false)
|
67
|
+
end
|
68
|
+
rescue IO::WaitReadable
|
69
|
+
next
|
70
|
+
rescue EOFError
|
71
|
+
streams.delete(io)
|
72
|
+
end
|
73
|
+
elsif !wait_thread.alive?
|
74
|
+
break
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
wait_thread.value
|
79
|
+
end
|
80
|
+
|
81
|
+
output_buffer.join
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class MCPTool < Tool
|
86
|
+
attr_reader :client, :tui
|
87
|
+
|
88
|
+
def initialize(client, tui, tool)
|
89
|
+
@client = client
|
90
|
+
@tui = tui
|
91
|
+
super(tool["name"], tool["description"], tool["inputSchema"] || {})
|
92
|
+
end
|
93
|
+
|
94
|
+
def call(args)
|
95
|
+
unless client.connected?
|
96
|
+
tui.say("MCP connection lost", colour: :red)
|
97
|
+
return ""
|
98
|
+
end
|
99
|
+
|
100
|
+
result = client.call(name, args)
|
101
|
+
tui.say(result)
|
102
|
+
|
103
|
+
if result.nil? || result.empty?
|
104
|
+
tui.say("Tool call failed: no response from MCP server", colour: :red)
|
105
|
+
return result
|
106
|
+
end
|
107
|
+
|
108
|
+
if result["error"]
|
109
|
+
tui.say(result["error"], colour: :red)
|
110
|
+
return result
|
111
|
+
end
|
112
|
+
|
113
|
+
result.dig("content", 0, "text") || result.to_s
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/elelem/tools.rb
CHANGED
@@ -2,61 +2,32 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Tools
|
5
|
-
|
6
|
-
|
7
|
-
type: "function",
|
8
|
-
function: {
|
9
|
-
name: "execute_command",
|
10
|
-
description: "Execute a shell command.",
|
11
|
-
parameters: {
|
12
|
-
type: "object",
|
13
|
-
properties: {
|
14
|
-
command: { type: "string" },
|
15
|
-
},
|
16
|
-
required: ["command"]
|
17
|
-
}
|
18
|
-
},
|
19
|
-
handler: lambda { |args|
|
20
|
-
stdout, stderr, _status = Open3.capture3("/bin/sh", "-c", args["command"])
|
21
|
-
stdout + stderr
|
22
|
-
}
|
23
|
-
},
|
24
|
-
]
|
25
|
-
|
26
|
-
def initialize(tools = DEFAULT_TOOLS)
|
5
|
+
def initialize(configuration, tools)
|
6
|
+
@configuration = configuration
|
27
7
|
@tools = tools
|
28
8
|
end
|
29
9
|
|
30
10
|
def banner
|
31
|
-
|
32
|
-
[
|
33
|
-
h.dig(:function, :name),
|
34
|
-
h.dig(:function, :description)
|
35
|
-
].join(": ")
|
36
|
-
end.sort.join("\n ")
|
11
|
+
tools.map(&:banner).sort.join("\n ")
|
37
12
|
end
|
38
13
|
|
39
14
|
def execute(tool_call)
|
40
15
|
name = tool_call.dig("function", "name")
|
41
16
|
args = tool_call.dig("function", "arguments")
|
42
17
|
|
43
|
-
tool =
|
44
|
-
|
45
|
-
|
46
|
-
|
18
|
+
tool = tools.find { |tool| tool.name == name }
|
19
|
+
return "Invalid function name: #{name}" if tool.nil?
|
20
|
+
return "Invalid function arguments: #{args}" unless tool.valid?(args)
|
21
|
+
|
22
|
+
tool.call(args)
|
47
23
|
end
|
48
24
|
|
49
25
|
def to_h
|
50
|
-
|
51
|
-
{
|
52
|
-
type: tool[:type],
|
53
|
-
function: {
|
54
|
-
name: tool.dig(:function, :name),
|
55
|
-
description: tool.dig(:function, :description),
|
56
|
-
parameters: tool.dig(:function, :parameters)
|
57
|
-
}
|
58
|
-
}
|
59
|
-
end
|
26
|
+
tools.map(&:to_h)
|
60
27
|
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :configuration, :tools
|
61
32
|
end
|
62
33
|
end
|
data/lib/elelem/tui.rb
CHANGED
@@ -10,8 +10,7 @@ module Elelem
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def prompt(message)
|
13
|
-
|
14
|
-
stdin.gets&.chomp
|
13
|
+
Reline.readline(message, true)
|
15
14
|
end
|
16
15
|
|
17
16
|
def say(message, colour: :default, newline: false)
|
@@ -24,10 +23,50 @@ module Elelem
|
|
24
23
|
stdout.flush
|
25
24
|
end
|
26
25
|
|
26
|
+
def show_progress(message, prefix = "[.]", colour: :gray)
|
27
|
+
timestamp = current_time_string
|
28
|
+
formatted_message = colourize("#{prefix} #{timestamp} #{message}", colour: colour)
|
29
|
+
stdout.print(formatted_message)
|
30
|
+
stdout.flush
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear_line
|
34
|
+
stdout.print("\r#{" " * 80}\r")
|
35
|
+
stdout.flush
|
36
|
+
end
|
37
|
+
|
38
|
+
def complete_progress(message = "Completed")
|
39
|
+
clear_line
|
40
|
+
timestamp = current_time_string
|
41
|
+
formatted_message = colourize("[✓] #{timestamp} #{message}", colour: :green)
|
42
|
+
stdout.puts(formatted_message)
|
43
|
+
stdout.flush
|
44
|
+
end
|
45
|
+
|
27
46
|
private
|
28
47
|
|
48
|
+
def current_time_string
|
49
|
+
Time.now.strftime("%H:%M:%S")
|
50
|
+
end
|
51
|
+
|
29
52
|
def colourize(text, colour: :default)
|
30
53
|
case colour
|
54
|
+
when :black
|
55
|
+
"\e[30m#{text}\e[0m"
|
56
|
+
when :red
|
57
|
+
"\e[31m#{text}\e[0m"
|
58
|
+
when :green
|
59
|
+
"\e[32m#{text}\e[0m"
|
60
|
+
when :yellow
|
61
|
+
"\e[33m#{text}\e[0m"
|
62
|
+
when :blue
|
63
|
+
"\e[34m#{text}\e[0m"
|
64
|
+
when :magenta
|
65
|
+
"\e[35m#{text}\e[0m"
|
66
|
+
when :cyan
|
67
|
+
"\e[36m#{text}\e[0m"
|
68
|
+
when :white
|
69
|
+
"\e[37m#{text}\e[0m"
|
31
70
|
when :gray
|
32
71
|
"\e[90m#{text}\e[0m"
|
33
72
|
else
|
data/lib/elelem/version.rb
CHANGED
data/lib/elelem.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "erb"
|
3
4
|
require "json"
|
5
|
+
require "json-schema"
|
4
6
|
require "logger"
|
5
7
|
require "net/http"
|
6
8
|
require "open3"
|
9
|
+
require "reline"
|
7
10
|
require "thor"
|
8
11
|
require "uri"
|
9
12
|
|
@@ -12,7 +15,9 @@ require_relative "elelem/api"
|
|
12
15
|
require_relative "elelem/application"
|
13
16
|
require_relative "elelem/configuration"
|
14
17
|
require_relative "elelem/conversation"
|
18
|
+
require_relative "elelem/mcp_client"
|
15
19
|
require_relative "elelem/state"
|
20
|
+
require_relative "elelem/tool"
|
16
21
|
require_relative "elelem/tools"
|
17
22
|
require_relative "elelem/tui"
|
18
23
|
require_relative "elelem/version"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elelem
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mo khan
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: erb
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
12
26
|
- !ruby/object:Gem::Dependency
|
13
27
|
name: json
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -23,6 +37,20 @@ dependencies:
|
|
23
37
|
- - ">="
|
24
38
|
- !ruby/object:Gem::Version
|
25
39
|
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: json-schema
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
26
54
|
- !ruby/object:Gem::Dependency
|
27
55
|
name: logger
|
28
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -65,6 +93,20 @@ dependencies:
|
|
65
93
|
- - ">="
|
66
94
|
- !ruby/object:Gem::Version
|
67
95
|
version: '0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: reline
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
68
110
|
- !ruby/object:Gem::Dependency
|
69
111
|
name: thor
|
70
112
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,8 +143,6 @@ executables:
|
|
101
143
|
extensions: []
|
102
144
|
extra_rdoc_files: []
|
103
145
|
files:
|
104
|
-
- ".rspec"
|
105
|
-
- ".rubocop.yml"
|
106
146
|
- CHANGELOG.md
|
107
147
|
- LICENSE.txt
|
108
148
|
- README.md
|
@@ -114,11 +154,13 @@ files:
|
|
114
154
|
- lib/elelem/application.rb
|
115
155
|
- lib/elelem/configuration.rb
|
116
156
|
- lib/elelem/conversation.rb
|
157
|
+
- lib/elelem/mcp_client.rb
|
117
158
|
- lib/elelem/state.rb
|
159
|
+
- lib/elelem/system_prompt.erb
|
160
|
+
- lib/elelem/tool.rb
|
118
161
|
- lib/elelem/tools.rb
|
119
162
|
- lib/elelem/tui.rb
|
120
163
|
- lib/elelem/version.rb
|
121
|
-
- mise.toml
|
122
164
|
- sig/elelem.rbs
|
123
165
|
homepage: https://www.mokhan.ca
|
124
166
|
licenses:
|
@@ -135,7 +177,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
135
177
|
requirements:
|
136
178
|
- - ">="
|
137
179
|
- !ruby/object:Gem::Version
|
138
|
-
version: 3.
|
180
|
+
version: 3.4.0
|
139
181
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
182
|
requirements:
|
141
183
|
- - ">="
|
data/.rspec
DELETED
data/.rubocop.yml
DELETED
data/mise.toml
DELETED