elelem 0.1.0 → 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 +12 -0
- data/README.md +130 -8
- data/exe/elelem +4 -1
- data/lib/elelem/agent.rb +34 -60
- data/lib/elelem/api.rb +35 -0
- data/lib/elelem/application.rb +30 -11
- data/lib/elelem/configuration.rb +31 -6
- data/lib/elelem/conversation.rb +16 -11
- data/lib/elelem/mcp_client.rb +99 -0
- data/lib/elelem/state.rb +162 -0
- data/lib/elelem/system_prompt.erb +7 -0
- data/lib/elelem/tool.rb +116 -0
- data/lib/elelem/tools.rb +15 -59
- data/lib/elelem/tui.rb +77 -0
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +8 -0
- metadata +50 -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
@@ -1,5 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.2] - 2025-08-14
|
4
|
+
|
5
|
+
### Fixed
|
6
|
+
- Fixed critical bug where bash tool had nested parameters schema causing tool calls to fail with "no implicit conversion of nil into String" error
|
7
|
+
|
8
|
+
## [0.1.1] - 2025-08-12
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
- Fixed infinite loop bug after tool execution - loop now continues until assistant provides final response
|
12
|
+
- Fixed conversation history accumulating streaming chunks as separate entries - now properly combines same-role consecutive messages
|
13
|
+
- Improved state machine logging with better debug output
|
14
|
+
|
3
15
|
## [0.1.0] - 2025-08-08
|
4
16
|
|
5
17
|
- Initial release
|
data/README.md
CHANGED
@@ -1,28 +1,56 @@
|
|
1
1
|
# Elelem
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/elelem`. To experiment with that code, run `bin/console` for an interactive prompt.
|
3
|
+
Elelem is an interactive REPL (Read-Eval-Print Loop) for Ollama that provides a command-line chat interface for communicating with AI models. It features tool calling capabilities, streaming responses, and a clean state machine architecture.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
-
|
11
7
|
Install the gem and add to the application's Gemfile by executing:
|
12
8
|
|
13
9
|
```bash
|
14
|
-
bundle add
|
10
|
+
bundle add elelem
|
15
11
|
```
|
16
12
|
|
17
13
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
18
14
|
|
19
15
|
```bash
|
20
|
-
gem install
|
16
|
+
gem install elelem
|
21
17
|
```
|
22
18
|
|
23
19
|
## Usage
|
24
20
|
|
25
|
-
|
21
|
+
Start an interactive chat session with an Ollama model:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
elelem chat
|
25
|
+
```
|
26
|
+
|
27
|
+
### Options
|
28
|
+
|
29
|
+
- `--host`: Specify Ollama host (default: localhost:11434)
|
30
|
+
- `--model`: Specify Ollama model (default: gpt-oss, currently only tested with gpt-oss)
|
31
|
+
- `--token`: Provide authentication token
|
32
|
+
- `--debug`: Enable debug logging
|
33
|
+
|
34
|
+
### Examples
|
35
|
+
|
36
|
+
```bash
|
37
|
+
# Chat with default model
|
38
|
+
elelem chat
|
39
|
+
|
40
|
+
# Chat with specific model and host
|
41
|
+
elelem chat --model llama2 --host remote-host:11434
|
42
|
+
|
43
|
+
# Enable debug mode
|
44
|
+
elelem chat --debug
|
45
|
+
```
|
46
|
+
|
47
|
+
### Features
|
48
|
+
|
49
|
+
- **Interactive REPL**: Clean command-line interface for chatting
|
50
|
+
- **Tool Execution**: Execute shell commands when requested by the AI
|
51
|
+
- **Streaming Responses**: Real-time streaming of AI responses
|
52
|
+
- **State Machine**: Robust state management for different interaction modes
|
53
|
+
- **Conversation History**: Maintains context across the session
|
26
54
|
|
27
55
|
## Development
|
28
56
|
|
@@ -30,6 +58,100 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
30
58
|
|
31
59
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
60
|
|
61
|
+
REPL State Diagram
|
62
|
+
|
63
|
+
```
|
64
|
+
┌─────────────────┐
|
65
|
+
│ START/INIT │
|
66
|
+
└─────────┬───────┘
|
67
|
+
│
|
68
|
+
v
|
69
|
+
┌─────────────────┐
|
70
|
+
┌────▶│ IDLE (Prompt) │◄────┐
|
71
|
+
│ │ Shows "> " │ │
|
72
|
+
│ └─────────┬───────┘ │
|
73
|
+
│ │ │
|
74
|
+
│ │ User input │
|
75
|
+
│ v │
|
76
|
+
│ ┌─────────────────┐ │
|
77
|
+
│ │ PROCESSING │ │
|
78
|
+
│ │ INPUT │ │
|
79
|
+
│ └─────────┬───────┘ │
|
80
|
+
│ │ │
|
81
|
+
│ │ API call │
|
82
|
+
│ v │
|
83
|
+
│ ┌─────────────────┐ │
|
84
|
+
│ │ STREAMING │ │
|
85
|
+
│ ┌──▶│ RESPONSE │─────┤
|
86
|
+
│ │ └─────────┬───────┘ │
|
87
|
+
│ │ │ │ done=true
|
88
|
+
│ │ │ Parse chunk │
|
89
|
+
│ │ v │
|
90
|
+
│ │ ┌─────────────────┐ │
|
91
|
+
│ │ │ MESSAGE TYPE │ │
|
92
|
+
│ │ │ ROUTING │ │
|
93
|
+
│ │ └─────┬─┬─┬───────┘ │
|
94
|
+
│ │ │ │ │ │
|
95
|
+
┌────────┴─┴─────────┘ │ └─────────────┴──────────┐
|
96
|
+
│ │ │
|
97
|
+
v v v
|
98
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
99
|
+
│ THINKING │ │ TOOL │ │ CONTENT │
|
100
|
+
│ STATE │ │ EXECUTION │ │ OUTPUT │
|
101
|
+
│ │ │ STATE │ │ STATE │
|
102
|
+
└─────────────┘ └─────┬───────┘ └─────────────┘
|
103
|
+
│ │ │
|
104
|
+
│ │ done=false │
|
105
|
+
└───────────────────┼──────────────────────────┘
|
106
|
+
│
|
107
|
+
v
|
108
|
+
┌─────────────────┐
|
109
|
+
│ CONTINUE │
|
110
|
+
│ STREAMING │
|
111
|
+
└─────────────────┘
|
112
|
+
│
|
113
|
+
└─────────────────┐
|
114
|
+
│
|
115
|
+
┌─────────────────┐ │
|
116
|
+
│ ERROR STATE │ │
|
117
|
+
│ (Exception) │ │
|
118
|
+
└─────────────────┘ │
|
119
|
+
▲ │
|
120
|
+
│ Invalid response │
|
121
|
+
└────────────────────────────┘
|
122
|
+
|
123
|
+
EXIT CONDITIONS:
|
124
|
+
┌─────────────────────────┐
|
125
|
+
│ • User enters "" │
|
126
|
+
│ • User enters "exit" │
|
127
|
+
│ • EOF (Ctrl+D) │
|
128
|
+
│ • nil input │
|
129
|
+
└─────────────────────────┘
|
130
|
+
│
|
131
|
+
v
|
132
|
+
┌─────────────────────────┐
|
133
|
+
│ TERMINATE │
|
134
|
+
└─────────────────────────┘
|
135
|
+
```
|
136
|
+
|
137
|
+
Key Transitions:
|
138
|
+
|
139
|
+
1. IDLE → PROCESSING: User enters any non-empty, non-"exit" input
|
140
|
+
2. PROCESSING → STREAMING: API call initiated to Ollama
|
141
|
+
3. STREAMING → MESSAGE ROUTING: Each chunk received is parsed
|
142
|
+
4. MESSAGE ROUTING → States: Based on message content:
|
143
|
+
- thinking → THINKING STATE
|
144
|
+
- tool_calls → TOOL EXECUTION STATE
|
145
|
+
- content → CONTENT OUTPUT STATE
|
146
|
+
- Invalid format → ERROR STATE
|
147
|
+
5. All States → IDLE: When done=true from API response
|
148
|
+
6. TOOL EXECUTION → STREAMING: Sets done=false to continue conversation
|
149
|
+
7. Any State → TERMINATE: On exit conditions
|
150
|
+
|
151
|
+
The REPL operates as a continuous loop where the primary flow is IDLE → PROCESSING → STREAMING →
|
152
|
+
back to IDLE, with the streaming phase potentially cycling through multiple message types before
|
153
|
+
completion.
|
154
|
+
|
33
155
|
## Contributing
|
34
156
|
|
35
157
|
Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/elelem.
|
data/exe/elelem
CHANGED
data/lib/elelem/agent.rb
CHANGED
@@ -2,86 +2,60 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Agent
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :api, :conversation, :logger, :model
|
6
6
|
|
7
7
|
def initialize(configuration)
|
8
|
+
@api = configuration.api
|
8
9
|
@configuration = configuration
|
10
|
+
@model = configuration.model
|
9
11
|
@conversation = configuration.conversation
|
10
|
-
@
|
12
|
+
@logger = configuration.logger
|
13
|
+
transition_to(Idle.new)
|
11
14
|
end
|
12
15
|
|
13
16
|
def repl
|
14
17
|
loop do
|
15
|
-
|
16
|
-
user = STDIN.gets&.chomp
|
17
|
-
break if user.nil? || user.empty? || user == 'exit'
|
18
|
-
process_input(user)
|
19
|
-
puts("\u001b[32mDone!\u001b[0m")
|
18
|
+
current_state.run(self)
|
20
19
|
end
|
21
20
|
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
done = false
|
29
|
-
loop do
|
30
|
-
call_api(conversation.history) do |chunk|
|
31
|
-
debug_print(chunk)
|
32
|
-
|
33
|
-
response = JSON.parse(chunk)
|
34
|
-
done = response['done']
|
35
|
-
message = response['message'] || {}
|
22
|
+
def transition_to(next_state)
|
23
|
+
logger.debug("Transition to: #{next_state.class.name}")
|
24
|
+
@current_state = next_state
|
25
|
+
end
|
36
26
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
message['tool_calls'].each do |t|
|
41
|
-
conversation.add(role: 'tool', content: tools.execute(t))
|
42
|
-
end
|
43
|
-
done = false
|
44
|
-
elsif message['content'].to_s.strip
|
45
|
-
print message['content'].to_s.strip
|
46
|
-
else
|
47
|
-
raise chunk.inspect
|
48
|
-
end
|
49
|
-
end
|
27
|
+
def prompt(message)
|
28
|
+
configuration.tui.prompt(message)
|
29
|
+
end
|
50
30
|
|
51
|
-
|
52
|
-
|
31
|
+
def say(message, colour: :default, newline: false)
|
32
|
+
configuration.tui.say(message, colour: colour, newline: newline)
|
53
33
|
end
|
54
34
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
stream: true,
|
60
|
-
keep_alive: '5m',
|
61
|
-
options: { temperature: 0.1 },
|
62
|
-
tools: tools.to_h
|
63
|
-
}
|
64
|
-
json_body = body.to_json
|
65
|
-
debug_print(json_body)
|
35
|
+
def execute(tool_call)
|
36
|
+
logger.debug("Execute: #{tool_call}")
|
37
|
+
configuration.tools.execute(tool_call)
|
38
|
+
end
|
66
39
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
req['Authorization'] = "Bearer #{configuration.token}" if configuration.token
|
40
|
+
def show_progress(message, prefix = "[.]", colour: :gray)
|
41
|
+
configuration.tui.show_progress(message, prefix, colour: colour)
|
42
|
+
end
|
71
43
|
|
72
|
-
|
73
|
-
|
44
|
+
def clear_line
|
45
|
+
configuration.tui.clear_line
|
46
|
+
end
|
74
47
|
|
75
|
-
|
76
|
-
|
77
|
-
yield(chunk) if block_given?
|
78
|
-
$stdout.flush
|
79
|
-
end
|
80
|
-
end
|
48
|
+
def complete_progress(message = "Completed")
|
49
|
+
configuration.tui.complete_progress(message)
|
81
50
|
end
|
82
51
|
|
83
|
-
def
|
84
|
-
|
52
|
+
def quit
|
53
|
+
logger.debug("Exiting...")
|
54
|
+
exit
|
85
55
|
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :configuration, :current_state
|
86
60
|
end
|
87
61
|
end
|
data/lib/elelem/api.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class Api
|
5
|
+
attr_reader :configuration
|
6
|
+
|
7
|
+
def initialize(configuration)
|
8
|
+
@configuration = configuration
|
9
|
+
end
|
10
|
+
|
11
|
+
def chat(messages, &block)
|
12
|
+
body = {
|
13
|
+
messages: messages,
|
14
|
+
model: configuration.model,
|
15
|
+
stream: true,
|
16
|
+
keep_alive: "5m",
|
17
|
+
options: { temperature: 0.1 },
|
18
|
+
tools: configuration.tools.to_h
|
19
|
+
}
|
20
|
+
configuration.logger.debug(JSON.pretty_generate(body))
|
21
|
+
json_body = body.to_json
|
22
|
+
|
23
|
+
req = Net::HTTP::Post.new(configuration.uri)
|
24
|
+
req["Content-Type"] = "application/json"
|
25
|
+
req.body = json_body
|
26
|
+
req["Authorization"] = "Bearer #{configuration.token}" if configuration.token
|
27
|
+
|
28
|
+
configuration.http.request(req) do |response|
|
29
|
+
raise response.inspect unless response.code == "200"
|
30
|
+
|
31
|
+
response.read_body(&block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/elelem/application.rb
CHANGED
@@ -2,31 +2,50 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Application < Thor
|
5
|
-
desc
|
6
|
-
method_option :help,
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
method_option :
|
5
|
+
desc "chat", "Start the REPL"
|
6
|
+
method_option :help,
|
7
|
+
aliases: "-h",
|
8
|
+
type: :boolean,
|
9
|
+
desc: "Display usage information"
|
10
|
+
method_option :host,
|
11
|
+
aliases: "--host",
|
12
|
+
type: :string,
|
13
|
+
desc: "Ollama host",
|
14
|
+
default: ENV.fetch("OLLAMA_HOST", "localhost:11434")
|
15
|
+
method_option :model,
|
16
|
+
aliases: "--model",
|
17
|
+
type: :string,
|
18
|
+
desc: "Ollama model",
|
19
|
+
default: ENV.fetch("OLLAMA_MODEL", "gpt-oss")
|
20
|
+
method_option :token,
|
21
|
+
aliases: "--token",
|
22
|
+
type: :string,
|
23
|
+
desc: "Ollama token",
|
24
|
+
default: ENV.fetch("OLLAMA_API_KEY", nil)
|
25
|
+
method_option :debug,
|
26
|
+
aliases: "--debug",
|
27
|
+
type: :boolean,
|
28
|
+
desc: "Debug mode",
|
29
|
+
default: false
|
11
30
|
def chat(*)
|
12
31
|
if options[:help]
|
13
|
-
invoke :help, [
|
32
|
+
invoke :help, ["chat"]
|
14
33
|
else
|
15
34
|
configuration = Configuration.new(
|
16
35
|
host: options[:host],
|
17
36
|
model: options[:model],
|
18
37
|
token: options[:token],
|
19
|
-
debug: options[:debug]
|
38
|
+
debug: options[:debug]
|
20
39
|
)
|
21
|
-
say "
|
22
|
-
say
|
40
|
+
say "Agent (#{configuration.model})", :green
|
41
|
+
say configuration.tools.banner.to_s, :green
|
23
42
|
|
24
43
|
agent = Agent.new(configuration)
|
25
44
|
agent.repl
|
26
45
|
end
|
27
46
|
end
|
28
47
|
|
29
|
-
desc
|
48
|
+
desc "version", "spandx version"
|
30
49
|
def version
|
31
50
|
puts "v#{Spandx::VERSION}"
|
32
51
|
end
|
data/lib/elelem/configuration.rb
CHANGED
@@ -18,11 +18,17 @@ module Elelem
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
def tui
|
22
|
+
@tui ||= TUI.new($stdin, $stdout)
|
23
|
+
end
|
24
|
+
|
25
|
+
def api
|
26
|
+
@api ||= Api.new(self)
|
27
|
+
end
|
28
|
+
|
21
29
|
def logger
|
22
|
-
@logger ||=
|
23
|
-
|
24
|
-
logger.formatter = ->(_, _, _, msg) { msg }
|
25
|
-
end
|
30
|
+
@logger ||= Logger.new(debug ? "elelem.log" : "/dev/null").tap do |logger|
|
31
|
+
logger.formatter = ->(_, _, _, message) { "#{message.to_s.strip}\n" }
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
@@ -35,13 +41,32 @@ module Elelem
|
|
35
41
|
end
|
36
42
|
|
37
43
|
def tools
|
38
|
-
@tools ||= Tools.new
|
44
|
+
@tools ||= Tools.new(self, [BashTool.new(self)] + mcp_tools)
|
39
45
|
end
|
40
46
|
|
41
47
|
private
|
42
48
|
|
43
49
|
def scheme
|
44
|
-
host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ?
|
50
|
+
host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
|
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
|
+
])
|
45
70
|
end
|
46
71
|
end
|
47
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
|
|
@@ -20,10 +13,22 @@ module Elelem
|
|
20
13
|
end
|
21
14
|
|
22
15
|
# :TODO truncate conversation history
|
23
|
-
def add(role:
|
16
|
+
def add(role: :user, content: "")
|
17
|
+
role = role.to_sym
|
24
18
|
raise "unknown role: #{role}" unless ROLES.include?(role)
|
19
|
+
return if content.nil? || content.empty?
|
20
|
+
|
21
|
+
if @items.last && @items.last[:role] == role
|
22
|
+
@items.last[:content] += content
|
23
|
+
else
|
24
|
+
@items.push({ role: role, content: content })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
25
29
|
|
26
|
-
|
30
|
+
def system_prompt
|
31
|
+
ERB.new(Pathname.new(__dir__).join("system_prompt.erb").read).result(binding)
|
27
32
|
end
|
28
33
|
end
|
29
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
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class Idle
|
5
|
+
def run(agent)
|
6
|
+
agent.logger.debug("Idling...")
|
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"
|
10
|
+
|
11
|
+
agent.conversation.add(role: :user, content: input)
|
12
|
+
agent.transition_to(Working.new)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def git_branch
|
18
|
+
`git branch --no-color --show-current --no-abbrev`.strip
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Working
|
23
|
+
class State
|
24
|
+
attr_reader :agent
|
25
|
+
|
26
|
+
def initialize(agent)
|
27
|
+
@agent = agent
|
28
|
+
end
|
29
|
+
|
30
|
+
def display_name
|
31
|
+
self.class.name.split("::").last
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Waiting < State
|
36
|
+
def process(message)
|
37
|
+
state_for(message)&.process(message)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def state_for(message)
|
43
|
+
if message["thinking"] && !message["thinking"].empty?
|
44
|
+
Thinking.new(agent)
|
45
|
+
elsif message["tool_calls"]&.any?
|
46
|
+
Executing.new(agent)
|
47
|
+
elsif message["content"] && !message["content"].empty?
|
48
|
+
Talking.new(agent)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Thinking < State
|
54
|
+
def initialize(agent)
|
55
|
+
super(agent)
|
56
|
+
@progress_shown = false
|
57
|
+
end
|
58
|
+
|
59
|
+
def process(message)
|
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
|
66
|
+
agent.say(message["thinking"], colour: :gray, newline: false)
|
67
|
+
self
|
68
|
+
else
|
69
|
+
agent.say("\n\n", newline: false)
|
70
|
+
Waiting.new(agent).process(message)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Executing < State
|
76
|
+
def process(message)
|
77
|
+
if message["tool_calls"]&.any?
|
78
|
+
message["tool_calls"].each do |tool_call|
|
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")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
Waiting.new(agent)
|
92
|
+
end
|
93
|
+
end
|
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
|
+
|
108
|
+
class Talking < State
|
109
|
+
def initialize(agent)
|
110
|
+
super(agent)
|
111
|
+
@progress_shown = false
|
112
|
+
end
|
113
|
+
|
114
|
+
def process(message)
|
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
|
121
|
+
agent.conversation.add(role: message["role"], content: message["content"])
|
122
|
+
agent.say(message["content"], colour: :default, newline: false)
|
123
|
+
self
|
124
|
+
else
|
125
|
+
agent.say("\n\n", newline: false)
|
126
|
+
Waiting.new(agent).process(message)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def run(agent)
|
132
|
+
agent.logger.debug("Working...")
|
133
|
+
agent.show_progress("Processing...", "[.]", colour: :cyan)
|
134
|
+
agent.say("\n\n", newline: false)
|
135
|
+
|
136
|
+
state = Waiting.new(agent)
|
137
|
+
done = false
|
138
|
+
|
139
|
+
loop do
|
140
|
+
agent.api.chat(agent.conversation.history) do |chunk|
|
141
|
+
response = JSON.parse(chunk)
|
142
|
+
message = normalize(response["message"] || {})
|
143
|
+
done = response["done"]
|
144
|
+
|
145
|
+
agent.logger.debug("#{state.display_name}: #{message}")
|
146
|
+
state = state.process(message)
|
147
|
+
end
|
148
|
+
|
149
|
+
break if state.nil?
|
150
|
+
break if done && agent.conversation.history.last[:role] != :tool
|
151
|
+
end
|
152
|
+
|
153
|
+
agent.transition_to(Idle.new)
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def normalize(message)
|
159
|
+
message.reject { |_key, value| value.empty? }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -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,76 +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: { command: { type: 'string' } },
|
14
|
-
required: ['command']
|
15
|
-
}
|
16
|
-
},
|
17
|
-
handler: -> (args) {
|
18
|
-
stdout, stderr, _status = Open3.capture3('/bin/sh', '-c', args['command'])
|
19
|
-
stdout + stderr
|
20
|
-
}
|
21
|
-
},
|
22
|
-
{
|
23
|
-
type: 'function',
|
24
|
-
function: {
|
25
|
-
name: 'ask_user',
|
26
|
-
description: 'Ask the user to answer a question.',
|
27
|
-
parameters: {
|
28
|
-
type: 'object',
|
29
|
-
properties: { question: { type: 'string' } },
|
30
|
-
required: ['question']
|
31
|
-
}
|
32
|
-
},
|
33
|
-
handler: ->(args) {
|
34
|
-
puts("\u001b[35m#{args['question']}\u001b[0m")
|
35
|
-
print "> "
|
36
|
-
STDIN.gets&.chomp
|
37
|
-
}
|
38
|
-
}
|
39
|
-
]
|
40
|
-
|
41
|
-
def initialize(tools = DEFAULT_TOOLS)
|
5
|
+
def initialize(configuration, tools)
|
6
|
+
@configuration = configuration
|
42
7
|
@tools = tools
|
43
8
|
end
|
44
9
|
|
45
10
|
def banner
|
46
|
-
|
47
|
-
[
|
48
|
-
h.dig(:function, :name),
|
49
|
-
h.dig(:function, :description)
|
50
|
-
].join(": ")
|
51
|
-
end.sort.join("\n ")
|
11
|
+
tools.map(&:banner).sort.join("\n ")
|
52
12
|
end
|
53
13
|
|
54
14
|
def execute(tool_call)
|
55
|
-
name = tool_call.dig(
|
56
|
-
args = tool_call.dig(
|
15
|
+
name = tool_call.dig("function", "name")
|
16
|
+
args = tool_call.dig("function", "arguments")
|
17
|
+
|
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)
|
57
21
|
|
58
|
-
tool
|
59
|
-
tool.dig(:function, :name) == name
|
60
|
-
end
|
61
|
-
tool.fetch(:handler).call(args)
|
22
|
+
tool.call(args)
|
62
23
|
end
|
63
24
|
|
64
25
|
def to_h
|
65
|
-
|
66
|
-
{
|
67
|
-
type: tool[:type],
|
68
|
-
function: {
|
69
|
-
name: tool.dig(:function, :name),
|
70
|
-
description: tool.dig(:function, :description),
|
71
|
-
parameters: tool.dig(:function, :parameters)
|
72
|
-
}
|
73
|
-
}
|
74
|
-
end
|
26
|
+
tools.map(&:to_h)
|
75
27
|
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :configuration, :tools
|
76
32
|
end
|
77
33
|
end
|
data/lib/elelem/tui.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class TUI
|
5
|
+
attr_reader :stdin, :stdout
|
6
|
+
|
7
|
+
def initialize(stdin = $stdin, stdout = $stdout)
|
8
|
+
@stdin = stdin
|
9
|
+
@stdout = stdout
|
10
|
+
end
|
11
|
+
|
12
|
+
def prompt(message)
|
13
|
+
Reline.readline(message, true)
|
14
|
+
end
|
15
|
+
|
16
|
+
def say(message, colour: :default, newline: false)
|
17
|
+
formatted_message = colourize(message, colour: colour)
|
18
|
+
if newline
|
19
|
+
stdout.puts(formatted_message)
|
20
|
+
else
|
21
|
+
stdout.print(formatted_message)
|
22
|
+
end
|
23
|
+
stdout.flush
|
24
|
+
end
|
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
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def current_time_string
|
49
|
+
Time.now.strftime("%H:%M:%S")
|
50
|
+
end
|
51
|
+
|
52
|
+
def colourize(text, colour: :default)
|
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"
|
70
|
+
when :gray
|
71
|
+
"\e[90m#{text}\e[0m"
|
72
|
+
else
|
73
|
+
text
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/elelem/version.rb
CHANGED
data/lib/elelem.rb
CHANGED
@@ -1,17 +1,25 @@
|
|
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
|
|
10
13
|
require_relative "elelem/agent"
|
14
|
+
require_relative "elelem/api"
|
11
15
|
require_relative "elelem/application"
|
12
16
|
require_relative "elelem/configuration"
|
13
17
|
require_relative "elelem/conversation"
|
18
|
+
require_relative "elelem/mcp_client"
|
19
|
+
require_relative "elelem/state"
|
20
|
+
require_relative "elelem/tool"
|
14
21
|
require_relative "elelem/tools"
|
22
|
+
require_relative "elelem/tui"
|
15
23
|
require_relative "elelem/version"
|
16
24
|
|
17
25
|
module Elelem
|
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
|
@@ -110,12 +150,17 @@ files:
|
|
110
150
|
- exe/elelem
|
111
151
|
- lib/elelem.rb
|
112
152
|
- lib/elelem/agent.rb
|
153
|
+
- lib/elelem/api.rb
|
113
154
|
- lib/elelem/application.rb
|
114
155
|
- lib/elelem/configuration.rb
|
115
156
|
- lib/elelem/conversation.rb
|
157
|
+
- lib/elelem/mcp_client.rb
|
158
|
+
- lib/elelem/state.rb
|
159
|
+
- lib/elelem/system_prompt.erb
|
160
|
+
- lib/elelem/tool.rb
|
116
161
|
- lib/elelem/tools.rb
|
162
|
+
- lib/elelem/tui.rb
|
117
163
|
- lib/elelem/version.rb
|
118
|
-
- mise.toml
|
119
164
|
- sig/elelem.rbs
|
120
165
|
homepage: https://www.mokhan.ca
|
121
166
|
licenses:
|
@@ -132,7 +177,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
132
177
|
requirements:
|
133
178
|
- - ">="
|
134
179
|
- !ruby/object:Gem::Version
|
135
|
-
version: 3.
|
180
|
+
version: 3.4.0
|
136
181
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
182
|
requirements:
|
138
183
|
- - ">="
|
data/.rspec
DELETED
data/.rubocop.yml
DELETED
data/mise.toml
DELETED