elelem 0.1.2 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a5469d2d253c6e0d09f60de69dafffdffb305da3f5864a4f6aa9bbac5ae7da6
4
- data.tar.gz: 60d9d35b759e0722b557cefbb4fb6d6b1713661de432efb8283ae1cd7f951e05
3
+ metadata.gz: 9d4a99b7addd5861f402c297ecfd19bc52e8edc6bba89422c2dec195a8cdcd13
4
+ data.tar.gz: d663221598cb4a843879ae191d6b7d8f62318be15d121803f58f752cecc79bbc
5
5
  SHA512:
6
- metadata.gz: 46073936ca9abcf83897e83355b7f979ebf2866d16351ccced55d70a8c311d8068acb3a78126b85b7b6622b45e6b2d51dc86f35914a25fdec937ad60214e4da9
7
- data.tar.gz: d7233c12ed3b2359c5c7a1d47e908951e0651f19fb8d07777e34aa0fb5ddfc51f4368a7a192136a743d1009a9a26239da572555dd5d3d90e99e1a4e2668c2d47
6
+ metadata.gz: cdc7ccfb94de895a32f5be4cc310f23ca781119628fcbec4bd95a50df2c5b0eb3caa474c3a9058b6120c744e56f53001eb8c67b09cad7934f9b03292202e3c88
7
+ data.tar.gz: 9732e722f6d1c040d4c497b6826bd53ac425ad129befdd37da0e7ecd60b9bf4e6c96464a44f89a4cd118ca79b5b675ac6190ebf157c21bf4d06aa8ced13507f9
data/exe/elelem CHANGED
@@ -3,9 +3,6 @@
3
3
 
4
4
  require "elelem"
5
5
 
6
- Reline.input = $stdin
7
- Reline.output = $stdout
8
-
9
6
  Signal.trap("INT") do
10
7
  exit(1)
11
8
  end
data/lib/elelem/agent.rb CHANGED
@@ -2,15 +2,19 @@
2
2
 
3
3
  module Elelem
4
4
  class Agent
5
- attr_reader :api, :conversation, :logger, :model
5
+ attr_reader :api, :conversation, :logger, :model, :tui
6
6
 
7
7
  def initialize(configuration)
8
8
  @api = configuration.api
9
+ @tui = configuration.tui
9
10
  @configuration = configuration
10
11
  @model = configuration.model
11
12
  @conversation = configuration.conversation
12
13
  @logger = configuration.logger
13
- transition_to(Idle.new)
14
+
15
+ at_exit { cleanup }
16
+
17
+ transition_to(States::Idle.new)
14
18
  end
15
19
 
16
20
  def repl
@@ -24,36 +28,22 @@ module Elelem
24
28
  @current_state = next_state
25
29
  end
26
30
 
27
- def prompt(message)
28
- configuration.tui.prompt(message)
29
- end
30
-
31
- def say(message, colour: :default, newline: false)
32
- configuration.tui.say(message, colour: colour, newline: newline)
33
- end
34
-
35
31
  def execute(tool_call)
36
32
  logger.debug("Execute: #{tool_call}")
37
33
  configuration.tools.execute(tool_call)
38
34
  end
39
35
 
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
-
52
36
  def quit
53
37
  logger.debug("Exiting...")
38
+ cleanup
54
39
  exit
55
40
  end
56
41
 
42
+ def cleanup
43
+ logger.debug("Cleaning up agent...")
44
+ configuration.cleanup
45
+ end
46
+
57
47
  private
58
48
 
59
49
  attr_reader :configuration, :current_state
@@ -45,9 +45,9 @@ module Elelem
45
45
  end
46
46
  end
47
47
 
48
- desc "version", "spandx version"
48
+ desc "version", "The version of this CLI"
49
49
  def version
50
- puts "v#{Spandx::VERSION}"
50
+ say "v#{Elelem::VERSION}"
51
51
  end
52
52
  map %w[--version -v] => :version
53
53
  end
@@ -37,11 +37,22 @@ module Elelem
37
37
  end
38
38
 
39
39
  def conversation
40
- @conversation ||= Conversation.new
40
+ @conversation ||= Conversation.new.tap do |conversation|
41
+ resources = mcp_clients.map do |client|
42
+ client.resources.map do |resource|
43
+ resource["uri"]
44
+ end
45
+ end.flatten
46
+ conversation.add(role: :tool, content: resources)
47
+ end
41
48
  end
42
49
 
43
50
  def tools
44
- @tools ||= Tools.new(self, [BashTool.new(self)] + mcp_tools)
51
+ @tools ||= Tools.new(self, [Toolbox::Bash.new(self)] + mcp_tools)
52
+ end
53
+
54
+ def cleanup
55
+ @mcp_clients&.each(&:shutdown)
45
56
  end
46
57
 
47
58
  private
@@ -50,23 +61,23 @@ module Elelem
50
61
  host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
51
62
  end
52
63
 
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
64
+ def mcp_tools
65
+ @mcp_tools ||= mcp_clients.map do |client|
66
+ client.tools.map do |tool|
67
+ Toolbox::MCP.new(client, tui, tool)
68
+ end
69
+ end.flatten
57
70
  end
58
71
 
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
- ])
72
+ def mcp_clients
73
+ @mcp_clients ||= begin
74
+ config = Pathname.pwd.join(".mcp.json")
75
+ return [] unless config.exist?
76
+
77
+ JSON.parse(config.read).map do |_key, value|
78
+ MCPClient.new(self, [value["command"]] + value["args"])
79
+ end
80
+ end
70
81
  end
71
82
  end
72
83
  end
@@ -21,7 +21,7 @@ module Elelem
21
21
  if @items.last && @items.last[:role] == role
22
22
  @items.last[:content] += content
23
23
  else
24
- @items.push({ role: role, content: content })
24
+ @items.push({ role: role, content: normalize(content) })
25
25
  end
26
26
  end
27
27
 
@@ -30,5 +30,13 @@ module Elelem
30
30
  def system_prompt
31
31
  ERB.new(Pathname.new(__dir__).join("system_prompt.erb").read).result(binding)
32
32
  end
33
+
34
+ def normalize(content)
35
+ if content.is_a?(Array)
36
+ content.join(", ")
37
+ else
38
+ content.to_s
39
+ end
40
+ end
33
41
  end
34
42
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Elelem
4
4
  class MCPClient
5
- attr_reader :tools
5
+ attr_reader :tools, :resources
6
6
 
7
7
  def initialize(configuration, command = [])
8
8
  @configuration = configuration
@@ -12,7 +12,7 @@ module Elelem
12
12
  send_request(
13
13
  method: "initialize",
14
14
  params: {
15
- protocolVersion: "2024-11-05",
15
+ protocolVersion: "2025-06-08",
16
16
  capabilities: {
17
17
  tools: {}
18
18
  },
@@ -23,11 +23,12 @@ module Elelem
23
23
  }
24
24
  )
25
25
 
26
- # 2. Send initialized notification (required by MCP protocol)
26
+ # 2. Send initialized notification (optional for some MCP servers)
27
27
  send_notification(method: "notifications/initialized")
28
28
 
29
29
  # 3. Now we can request tools
30
30
  @tools = send_request(method: "tools/list")&.dig("tools") || []
31
+ @resources = send_request(method: "resources/list")&.dig("resources") || []
31
32
  end
32
33
 
33
34
  def connected?
@@ -53,6 +54,33 @@ module Elelem
53
54
  )
54
55
  end
55
56
 
57
+ def shutdown
58
+ return unless connected?
59
+
60
+ configuration.logger.debug("Shutting down MCP client")
61
+
62
+ [@stdin, @stdout, @stderr].each do |stream|
63
+ stream&.close unless stream&.closed?
64
+ end
65
+
66
+ return unless @worker&.alive?
67
+
68
+ begin
69
+ Process.kill("TERM", @worker.pid)
70
+ # Give it 2 seconds to terminate gracefully
71
+ Timeout.timeout(2) { @worker.value }
72
+ rescue Timeout::Error
73
+ # Force kill if it doesn't respond
74
+ begin
75
+ Process.kill("KILL", @worker.pid)
76
+ rescue StandardError
77
+ nil
78
+ end
79
+ rescue Errno::ESRCH
80
+ # Process already dead
81
+ end
82
+ end
83
+
56
84
  private
57
85
 
58
86
  attr_reader :stdin, :stdout, :stderr, :worker, :configuration
@@ -86,6 +114,8 @@ module Elelem
86
114
  end
87
115
 
88
116
  def send_notification(method:, params: {})
117
+ return unless connected?
118
+
89
119
  notification = {
90
120
  jsonrpc: "2.0",
91
121
  method: method
@@ -94,6 +124,13 @@ module Elelem
94
124
  configuration.logger.debug("Sending notification: #{JSON.pretty_generate(notification)}")
95
125
  @stdin.puts(JSON.generate(notification))
96
126
  @stdin.flush
127
+
128
+ response_line = @stdout.gets&.strip
129
+ return {} if response_line.nil? || response_line.empty?
130
+
131
+ response = JSON.parse(response_line)
132
+ configuration.logger.debug(JSON.pretty_generate(response))
133
+ response
97
134
  end
98
135
  end
99
136
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module States
5
+ class Idle
6
+ def run(agent)
7
+ agent.logger.debug("Idling...")
8
+ agent.tui.say("#{Dir.pwd} (#{agent.model}) [#{git_branch}]", colour: :magenta, newline: true)
9
+ input = agent.tui.prompt("モ ")
10
+ agent.quit if input.nil? || input.empty? || input == "exit" || input == "quit"
11
+
12
+ agent.conversation.add(role: :user, content: input)
13
+ agent.transition_to(Working)
14
+ end
15
+
16
+ private
17
+
18
+ def git_branch
19
+ `git branch --no-color --show-current --no-abbrev`.strip
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module States
5
+ module Working
6
+ class Error < State
7
+ def initialize(agent, error_message)
8
+ super(agent, "X", :red)
9
+ @error_message = error_message
10
+ end
11
+
12
+ def process(_message)
13
+ agent.tui.say("\nTool execution failed: #{@error_message}", colour: :red)
14
+ Waiting.new(agent)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module States
5
+ module Working
6
+ class Executing < State
7
+ def process(message)
8
+ if message["tool_calls"]&.any?
9
+ message["tool_calls"].each do |tool_call|
10
+ agent.conversation.add(role: :tool, content: agent.execute(tool_call))
11
+ end
12
+ end
13
+
14
+ Waiting.new(agent)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module States
5
+ module Working
6
+ class State
7
+ attr_reader :agent
8
+
9
+ def initialize(agent, icon, colour)
10
+ @agent = agent
11
+
12
+ agent.logger.debug("#{display_name}...")
13
+ agent.tui.show_progress("#{display_name}...", icon, colour: colour)
14
+ end
15
+
16
+ def run(message)
17
+ process(message)
18
+ end
19
+
20
+ def display_name
21
+ self.class.name.split("::").last
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module States
5
+ module Working
6
+ class Talking < State
7
+ def process(message)
8
+ if message["content"] && !message["content"]&.empty?
9
+ agent.conversation.add(role: message["role"], content: message["content"])
10
+ agent.tui.say(message["content"], colour: :default, newline: false)
11
+ self
12
+ else
13
+ Waiting.new(agent).process(message)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module States
5
+ module Working
6
+ class Thinking < State
7
+ def process(message)
8
+ if message["thinking"] && !message["thinking"]&.empty?
9
+ agent.tui.say(message["thinking"], colour: :gray, newline: false)
10
+ self
11
+ else
12
+ Waiting.new(agent).process(message)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module States
5
+ module Working
6
+ class Waiting < State
7
+ def initialize(agent)
8
+ super(agent, ".", :cyan)
9
+ end
10
+
11
+ def process(message)
12
+ state_for(message)&.process(message)
13
+ end
14
+
15
+ private
16
+
17
+ def state_for(message)
18
+ if message["thinking"] && !message["thinking"].empty?
19
+ Thinking.new(agent, "*", :yellow)
20
+ elsif message["tool_calls"]&.any?
21
+ Executing.new(agent, ">", :magenta)
22
+ elsif message["content"] && !message["content"].empty?
23
+ Talking.new(agent, "~", :white)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module States
5
+ module Working
6
+ class << self
7
+ def run(agent)
8
+ done = false
9
+ state = Waiting.new(agent)
10
+
11
+ loop do
12
+ agent.api.chat(agent.conversation.history) do |chunk|
13
+ response = JSON.parse(chunk)
14
+ message = normalize(response["message"] || {})
15
+ done = response["done"]
16
+
17
+ agent.logger.debug("#{state.display_name}: #{message}")
18
+ state = state.run(message)
19
+ end
20
+
21
+ break if state.nil?
22
+ break if done && agent.conversation.history.last[:role] != :tool
23
+ end
24
+
25
+ agent.transition_to(States::Idle.new)
26
+ end
27
+
28
+ def normalize(message)
29
+ message.reject { |_key, value| value.empty? }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,7 +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.
1
+ **Shell Master** — bash>code; compose>write; pipe everything; /proc/sys native; automate fast; streams/transforms; POSIX+GNU; man(1) first; no cleverness.
2
2
 
3
3
  Time: `<%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %>`
4
4
  Project Directory: `<%= Dir.pwd %>`
5
5
  System Info: `<%= `uname -a`.strip %>`
6
6
 
7
- Del is now being connected with a person.
7
+ Ready to hack.
data/lib/elelem/tool.rb CHANGED
@@ -29,88 +29,4 @@ module Elelem
29
29
  }
30
30
  end
31
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
32
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Toolbox
5
+ class Bash < ::Elelem::Tool
6
+ attr_reader :tui
7
+
8
+ def initialize(configuration)
9
+ @tui = configuration.tui
10
+ super("bash", "Run commands in /bin/bash -c. Full access to filesystem, network, processes, and all Unix tools.", {
11
+ type: "object",
12
+ properties: {
13
+ command: { type: "string" }
14
+ },
15
+ required: ["command"]
16
+ })
17
+ end
18
+
19
+ def call(args)
20
+ command = args["command"]
21
+ output_buffer = []
22
+
23
+ Open3.popen3("/bin/bash", "-c", command) do |stdin, stdout, stderr, wait_thread|
24
+ stdin.close
25
+ streams = [stdout, stderr]
26
+
27
+ until streams.empty?
28
+ ready = IO.select(streams, nil, nil, 0.1)
29
+
30
+ if ready
31
+ ready[0].each do |io|
32
+ data = io.read_nonblock(4096)
33
+ output_buffer << data
34
+
35
+ if io == stderr
36
+ tui.say(data, colour: :red, newline: false)
37
+ else
38
+ tui.say(data, newline: false)
39
+ end
40
+ rescue IO::WaitReadable
41
+ next
42
+ rescue EOFError
43
+ streams.delete(io)
44
+ end
45
+ elsif !wait_thread.alive?
46
+ break
47
+ end
48
+ end
49
+
50
+ wait_thread.value
51
+ end
52
+
53
+ output_buffer.join
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Toolbox
5
+ class MCP < ::Elelem::Tool
6
+ attr_reader :client, :tui
7
+
8
+ def initialize(client, tui, tool)
9
+ @client = client
10
+ @tui = tui
11
+ super(tool["name"], tool["description"], tool["inputSchema"] || {})
12
+ end
13
+
14
+ def call(args)
15
+ unless client.connected?
16
+ tui.say("MCP connection lost", colour: :red)
17
+ return ""
18
+ end
19
+
20
+ result = client.call(name, args)
21
+ tui.say(JSON.pretty_generate(result), newline: true)
22
+
23
+ if result.nil? || result.empty?
24
+ tui.say("Tool call failed: no response from MCP server", colour: :red)
25
+ return result
26
+ end
27
+
28
+ if result["error"]
29
+ tui.say(result["error"], colour: :red)
30
+ return result
31
+ end
32
+
33
+ result.dig("content", 0, "text") || result.to_s
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/elelem/tools.rb CHANGED
@@ -19,7 +19,9 @@ module Elelem
19
19
  return "Invalid function name: #{name}" if tool.nil?
20
20
  return "Invalid function arguments: #{args}" unless tool.valid?(args)
21
21
 
22
- tool.call(args)
22
+ CLI::UI::Frame.open(name) do
23
+ tool.call(args)
24
+ end
23
25
  end
24
26
 
25
27
  def to_h
data/lib/elelem/tui.rb CHANGED
@@ -14,41 +14,30 @@ module Elelem
14
14
  end
15
15
 
16
16
  def say(message, colour: :default, newline: false)
17
- formatted_message = colourize(message, colour: colour)
18
17
  if newline
19
- stdout.puts(formatted_message)
18
+ stdout.puts(colourize(message, colour: colour))
20
19
  else
21
- stdout.print(formatted_message)
20
+ stdout.print(colourize(message, colour: colour))
22
21
  end
23
22
  stdout.flush
24
23
  end
25
24
 
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
25
+ def show_progress(message, icon = ".", colour: :gray)
26
+ timestamp = Time.now.strftime("%H:%M:%S")
27
+ say("\n[#{icon}] #{timestamp} #{message}", colour: colour, newline: true)
31
28
  end
32
29
 
33
30
  def clear_line
34
- stdout.print("\r#{" " * 80}\r")
35
- stdout.flush
31
+ say("\r#{" " * 80}\r", newline: false)
36
32
  end
37
33
 
38
34
  def complete_progress(message = "Completed")
39
35
  clear_line
40
- timestamp = current_time_string
41
- formatted_message = colourize("[✓] #{timestamp} #{message}", colour: :green)
42
- stdout.puts(formatted_message)
43
- stdout.flush
36
+ show_progress(message, "✓", colour: :green)
44
37
  end
45
38
 
46
39
  private
47
40
 
48
- def current_time_string
49
- Time.now.strftime("%H:%M:%S")
50
- end
51
-
52
41
  def colourize(text, colour: :default)
53
42
  case colour
54
43
  when :black
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/elelem.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cli/ui"
3
4
  require "erb"
4
5
  require "json"
5
6
  require "json-schema"
@@ -8,6 +9,7 @@ require "net/http"
8
9
  require "open3"
9
10
  require "reline"
10
11
  require "thor"
12
+ require "timeout"
11
13
  require "uri"
12
14
 
13
15
  require_relative "elelem/agent"
@@ -16,12 +18,25 @@ require_relative "elelem/application"
16
18
  require_relative "elelem/configuration"
17
19
  require_relative "elelem/conversation"
18
20
  require_relative "elelem/mcp_client"
19
- require_relative "elelem/state"
21
+ require_relative "elelem/states/idle"
22
+ require_relative "elelem/states/working"
23
+ require_relative "elelem/states/working/state"
24
+ require_relative "elelem/states/working/error"
25
+ require_relative "elelem/states/working/executing"
26
+ require_relative "elelem/states/working/talking"
27
+ require_relative "elelem/states/working/thinking"
28
+ require_relative "elelem/states/working/waiting"
20
29
  require_relative "elelem/tool"
30
+ require_relative "elelem/toolbox/bash"
31
+ require_relative "elelem/toolbox/mcp"
21
32
  require_relative "elelem/tools"
22
33
  require_relative "elelem/tui"
23
34
  require_relative "elelem/version"
24
35
 
36
+ CLI::UI::StdoutRouter.enable
37
+ Reline.input = $stdin
38
+ Reline.output = $stdout
39
+
25
40
  module Elelem
26
41
  class Error < StandardError; end
27
42
  end
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.2
4
+ version: 0.1.3
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: cli-ui
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: erb
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -121,6 +135,20 @@ dependencies:
121
135
  - - ">="
122
136
  - !ruby/object:Gem::Version
123
137
  version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: timeout
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
124
152
  - !ruby/object:Gem::Dependency
125
153
  name: uri
126
154
  requirement: !ruby/object:Gem::Requirement
@@ -155,9 +183,18 @@ files:
155
183
  - lib/elelem/configuration.rb
156
184
  - lib/elelem/conversation.rb
157
185
  - lib/elelem/mcp_client.rb
158
- - lib/elelem/state.rb
186
+ - lib/elelem/states/idle.rb
187
+ - lib/elelem/states/working.rb
188
+ - lib/elelem/states/working/error.rb
189
+ - lib/elelem/states/working/executing.rb
190
+ - lib/elelem/states/working/state.rb
191
+ - lib/elelem/states/working/talking.rb
192
+ - lib/elelem/states/working/thinking.rb
193
+ - lib/elelem/states/working/waiting.rb
159
194
  - lib/elelem/system_prompt.erb
160
195
  - lib/elelem/tool.rb
196
+ - lib/elelem/toolbox/bash.rb
197
+ - lib/elelem/toolbox/mcp.rb
161
198
  - lib/elelem/tools.rb
162
199
  - lib/elelem/tui.rb
163
200
  - lib/elelem/version.rb
data/lib/elelem/state.rb DELETED
@@ -1,162 +0,0 @@
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