elelem 0.1.0 → 0.1.1

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: 06a7abdf8e0f812a68d33858858674b65641c32df197305f3baff807bc7faa40
4
- data.tar.gz: fea3e4bf3d3c8fc2c8a9da954b704e8bb3542c5e003e5a8d4f9fcbe6a178152a
3
+ metadata.gz: 139cfa01fc1cb7c7a7d7c7b86fa72d49027dfeead68754dfd1be3d864228dbff
4
+ data.tar.gz: ee500832246d4a8c6e9c5c6aab4daadbbfe9b4c1724afe771de310a3dc402eb3
5
5
  SHA512:
6
- metadata.gz: 6790c0d2703328168d5abafa4b0b1e25987b771e60512edeb40e37f4b43cf10b4bd1e4e7ed9222c261238821f6610d9ba1f810c0f45bf32e7ef1a3b14dd4a553
7
- data.tar.gz: 85f83cb98ab8d2895c77910d36b2ba3b705f614bf3de0bccb48f41b48033f248edd316cc0219c4ff5b9a4fe3a10845f4280cc2723bc6841cbbbd1ea2ff97c7cf
6
+ metadata.gz: 920741eb6faf5bcfe67b78c62e65496012919a65d52b1919eefea0bedafd11f65de399c16d9bd6b3d812f2d21fd47303c8145267b9368d15cdba741a92dd50ef
7
+ data.tar.gz: e95155995fe0e21242bef5cd574699a47025b79064857183617b8d99c6d309885a226851db714ab2c8cb4b0a053055821b1f6b013df8e33b97034b0cd05bc270
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.1] - 2025-08-12
4
+
5
+ ### Fixed
6
+ - Fixed infinite loop bug after tool execution - loop now continues until assistant provides final response
7
+ - Fixed conversation history accumulating streaming chunks as separate entries - now properly combines same-role consecutive messages
8
+ - Improved state machine logging with better debug output
9
+
3
10
  ## [0.1.0] - 2025-08-08
4
11
 
5
12
  - Initial release
data/README.md CHANGED
@@ -1,28 +1,56 @@
1
1
  # Elelem
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
16
+ gem install elelem
21
17
  ```
22
18
 
23
19
  ## Usage
24
20
 
25
- TODO: Write usage instructions here
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
@@ -3,7 +3,7 @@
3
3
 
4
4
  require "elelem"
5
5
 
6
- Signal.trap('INT') do
6
+ Signal.trap("INT") do
7
7
  exit(1)
8
8
  end
9
9
 
data/lib/elelem/agent.rb CHANGED
@@ -2,86 +2,47 @@
2
2
 
3
3
  module Elelem
4
4
  class Agent
5
- attr_reader :configuration, :conversation, :tools
5
+ attr_reader :api, :conversation, :logger
6
6
 
7
7
  def initialize(configuration)
8
+ @api = configuration.api
8
9
  @configuration = configuration
9
10
  @conversation = configuration.conversation
10
- @tools = configuration.tools
11
+ @logger = configuration.logger
12
+ transition_to(Idle.new)
11
13
  end
12
14
 
13
15
  def repl
14
16
  loop do
15
- print "\n> "
16
- user = STDIN.gets&.chomp
17
- break if user.nil? || user.empty? || user == 'exit'
18
- process_input(user)
19
- puts("\u001b[32mDone!\u001b[0m")
17
+ current_state.run(self)
20
18
  end
21
19
  end
22
20
 
23
- private
24
-
25
- def process_input(text)
26
- conversation.add(role: 'user', content: text)
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'] || {}
36
-
37
- if message['thinking']
38
- print("\u001b[90m#{message['thinking']}\u001b[0m")
39
- elsif message['tool_calls']&.any?
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
50
-
51
- break if done
52
- end
21
+ def transition_to(next_state)
22
+ logger.debug("Transition to: #{next_state.class.name}")
23
+ @current_state = next_state
53
24
  end
54
25
 
55
- def call_api(messages)
56
- body = {
57
- messages: messages,
58
- model: configuration.model,
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)
66
-
67
- req = Net::HTTP::Post.new(configuration.uri)
68
- req['Content-Type'] = 'application/json'
69
- req.body = json_body
70
- req['Authorization'] = "Bearer #{configuration.token}" if configuration.token
26
+ def prompt(message)
27
+ configuration.tui.prompt(message)
28
+ end
71
29
 
72
- configuration.http.request(req) do |response|
73
- raise response.inspect unless response.code == "200"
30
+ def say(message, colour: :default, newline: false)
31
+ configuration.tui.say(message, colour: colour, newline: newline)
32
+ end
74
33
 
75
- response.read_body do |chunk|
76
- debug_print(chunk)
77
- yield(chunk) if block_given?
78
- $stdout.flush
79
- end
80
- end
34
+ def execute(tool_call)
35
+ logger.debug("Execute: #{tool_call}")
36
+ configuration.tools.execute(tool_call)
81
37
  end
82
38
 
83
- def debug_print(body = nil)
84
- configuration.logger.debug(body) if configuration.debug && body
39
+ def quit
40
+ logger.debug("Exiting...")
41
+ exit
85
42
  end
43
+
44
+ private
45
+
46
+ attr_reader :configuration, :current_state
86
47
  end
87
48
  end
data/lib/elelem/api.rb ADDED
@@ -0,0 +1,37 @@
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)
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 do |chunk|
32
+ yield(chunk)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -2,21 +2,40 @@
2
2
 
3
3
  module Elelem
4
4
  class Application < Thor
5
- desc 'chat', 'Start the REPL'
6
- method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information'
7
- method_option :host, aliases: '--host', type: :string, desc: 'Ollama host', default: ENV.fetch('OLLAMA_HOST', 'localhost:11434')
8
- method_option :model, aliases: '--model', type: :string, desc: 'Ollama model', default: ENV.fetch('OLLAMA_MODEL', 'gpt-oss')
9
- method_option :token, aliases: '--token', type: :string, desc: 'Ollama token', default: ENV.fetch('OLLAMA_API_KEY', nil)
10
- method_option :debug, aliases: '--debug', type: :boolean, desc: 'Debug mode', default: false
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, ['chat']
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
40
  say "Ollama Agent (#{configuration.model})", :green
22
41
  say "Tools:\n #{configuration.tools.banner}", :green
@@ -26,7 +45,7 @@ module Elelem
26
45
  end
27
46
  end
28
47
 
29
- desc 'version', 'spandx version'
48
+ desc "version", "spandx version"
30
49
  def version
31
50
  puts "v#{Spandx::VERSION}"
32
51
  end
@@ -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 ||= begin
23
- Logger.new(debug ? $stderr : "/dev/null").tap do |logger|
24
- logger.formatter = ->(_, _, _, msg) { msg }
25
- end
30
+ @logger ||= Logger.new(debug ? "elelem.log" : "/dev/null").tap do |logger|
31
+ logger.formatter = ->(_, _, _, message) { message.strip + "\n" }
26
32
  end
27
33
  end
28
34
 
@@ -41,7 +47,7 @@ module Elelem
41
47
  private
42
48
 
43
49
  def scheme
44
- host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? 'http' : 'https'
50
+ host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
45
51
  end
46
52
  end
47
53
  end
@@ -4,14 +4,14 @@ module Elelem
4
4
  class Conversation
5
5
  SYSTEM_MESSAGE = <<~SYS
6
6
  You are ChatGPT, a helpful assistant with reasoning capabilities.
7
- Current date: #{Time.now.strftime('%Y-%m-%d')}.
7
+ Current date: #{Time.now.strftime("%Y-%m-%d")}.
8
8
  System info: `uname -a` output: #{`uname -a`.strip}
9
9
  Reasoning: high
10
10
  SYS
11
11
 
12
- ROLES = ['system', 'user', 'tool'].freeze
12
+ ROLES = [:system, :assistant, :user, :tool].freeze
13
13
 
14
- def initialize(items = [{ role: 'system', content: SYSTEM_MESSAGE }])
14
+ def initialize(items = [{ role: "system", content: SYSTEM_MESSAGE }])
15
15
  @items = items
16
16
  end
17
17
 
@@ -20,10 +20,16 @@ module Elelem
20
20
  end
21
21
 
22
22
  # :TODO truncate conversation history
23
- def add(role: 'user', content: '')
23
+ def add(role: :user, content: "")
24
+ role = role.to_sym
24
25
  raise "unknown role: #{role}" unless ROLES.include?(role)
26
+ return if content.empty?
25
27
 
26
- @items << { role: role, content: content }
28
+ if @items.last && @items.last[:role] == role
29
+ @items.last[:content] += content
30
+ else
31
+ @items.push({ role: role, content: content })
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ class Idle
5
+ def run(agent)
6
+ agent.logger.debug("Idling...")
7
+ input = agent.prompt("\n> ")
8
+ agent.quit if input.nil? || input.empty? || input == "exit"
9
+
10
+ agent.conversation.add(role: :user, content: input)
11
+ agent.transition_to(Working.new)
12
+ end
13
+ end
14
+
15
+ class Working
16
+ class State
17
+ attr_reader :agent
18
+
19
+ def initialize(agent)
20
+ @agent = agent
21
+ end
22
+
23
+ def display_name
24
+ self.class.name.split("::").last
25
+ end
26
+ end
27
+
28
+ class Waiting < State
29
+ def process(message)
30
+ state = self
31
+
32
+ if message["thinking"] && !message["thinking"].empty?
33
+ state = Thinking.new(agent)
34
+ elsif message["tool_calls"]&.any?
35
+ state = Executing.new(agent)
36
+ elsif message["content"] && !message["content"].empty?
37
+ state = Talking.new(agent)
38
+ else
39
+ state = nil
40
+ end
41
+
42
+ state&.process(message)
43
+ end
44
+ end
45
+
46
+ class Thinking < State
47
+ def process(message)
48
+ if message["thinking"] && !message["thinking"]&.empty?
49
+ agent.say(message["thinking"], colour: :gray, newline: false)
50
+ self
51
+ else
52
+ agent.say("", newline: true)
53
+ Waiting.new(agent).process(message)
54
+ end
55
+ end
56
+ end
57
+
58
+ class Executing < State
59
+ def process(message)
60
+ if message["tool_calls"]&.any?
61
+ message["tool_calls"].each do |tool_call|
62
+ agent.conversation.add(role: :tool, content: agent.execute(tool_call))
63
+ end
64
+ end
65
+
66
+ Waiting.new(agent)
67
+ end
68
+ end
69
+
70
+ class Talking < State
71
+ def process(message)
72
+ if message["content"] && !message["content"]&.empty?
73
+ agent.conversation.add(role: message["role"], content: message["content"])
74
+ agent.say(message["content"], colour: :default, newline: false)
75
+ self
76
+ else
77
+ agent.say("", newline: true)
78
+ Waiting.new(agent).process(message)
79
+ end
80
+ end
81
+ end
82
+
83
+ def run(agent)
84
+ agent.logger.debug("Working...")
85
+ state = Waiting.new(agent)
86
+ done = false
87
+
88
+ loop do
89
+ agent.api.chat(agent.conversation.history) do |chunk|
90
+ response = JSON.parse(chunk)
91
+ message = normalize(response["message"] || {})
92
+ done = response["done"]
93
+
94
+ agent.logger.debug("#{state.display_name}: #{message}")
95
+ state = state.process(message)
96
+ end
97
+
98
+ break if state.nil?
99
+ break if done && agent.conversation.history.last[:role] != :tool
100
+ end
101
+
102
+ agent.transition_to(Idle.new)
103
+ end
104
+
105
+ private
106
+
107
+ def normalize(message)
108
+ message.reject { |_key, value| value.empty? }
109
+ end
110
+ end
111
+ end
data/lib/elelem/tools.rb CHANGED
@@ -4,38 +4,23 @@ module Elelem
4
4
  class Tools
5
5
  DEFAULT_TOOLS = [
6
6
  {
7
- type: 'function',
7
+ type: "function",
8
8
  function: {
9
- name: 'execute_command',
10
- description: 'Execute a shell command.',
9
+ name: "execute_command",
10
+ description: "Execute a shell command.",
11
11
  parameters: {
12
- type: 'object',
13
- properties: { command: { type: 'string' } },
14
- required: ['command']
12
+ type: "object",
13
+ properties: {
14
+ command: { type: "string" },
15
+ },
16
+ required: ["command"]
15
17
  }
16
18
  },
17
- handler: -> (args) {
18
- stdout, stderr, _status = Open3.capture3('/bin/sh', '-c', args['command'])
19
+ handler: lambda { |args|
20
+ stdout, stderr, _status = Open3.capture3("/bin/sh", "-c", args["command"])
19
21
  stdout + stderr
20
22
  }
21
23
  },
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
24
  ]
40
25
 
41
26
  def initialize(tools = DEFAULT_TOOLS)
@@ -52,13 +37,13 @@ module Elelem
52
37
  end
53
38
 
54
39
  def execute(tool_call)
55
- name = tool_call.dig('function', 'name')
56
- args = tool_call.dig('function', 'arguments')
40
+ name = tool_call.dig("function", "name")
41
+ args = tool_call.dig("function", "arguments")
57
42
 
58
43
  tool = @tools.find do |tool|
59
44
  tool.dig(:function, :name) == name
60
45
  end
61
- tool.fetch(:handler).call(args)
46
+ tool&.fetch(:handler)&.call(args)
62
47
  end
63
48
 
64
49
  def to_h
data/lib/elelem/tui.rb ADDED
@@ -0,0 +1,38 @@
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
+ say(message)
14
+ stdin.gets&.chomp
15
+ end
16
+
17
+ def say(message, colour: :default, newline: false)
18
+ formatted_message = colourize(message, colour: colour)
19
+ if newline
20
+ stdout.puts(formatted_message)
21
+ else
22
+ stdout.print(formatted_message)
23
+ end
24
+ stdout.flush
25
+ end
26
+
27
+ private
28
+
29
+ def colourize(text, colour: :default)
30
+ case colour
31
+ when :gray
32
+ "\e[90m#{text}\e[0m"
33
+ else
34
+ text
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
data/lib/elelem.rb CHANGED
@@ -8,10 +8,13 @@ require "thor"
8
8
  require "uri"
9
9
 
10
10
  require_relative "elelem/agent"
11
+ require_relative "elelem/api"
11
12
  require_relative "elelem/application"
12
13
  require_relative "elelem/configuration"
13
14
  require_relative "elelem/conversation"
15
+ require_relative "elelem/state"
14
16
  require_relative "elelem/tools"
17
+ require_relative "elelem/tui"
15
18
  require_relative "elelem/version"
16
19
 
17
20
  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.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -110,10 +110,13 @@ files:
110
110
  - exe/elelem
111
111
  - lib/elelem.rb
112
112
  - lib/elelem/agent.rb
113
+ - lib/elelem/api.rb
113
114
  - lib/elelem/application.rb
114
115
  - lib/elelem/configuration.rb
115
116
  - lib/elelem/conversation.rb
117
+ - lib/elelem/state.rb
116
118
  - lib/elelem/tools.rb
119
+ - lib/elelem/tui.rb
117
120
  - lib/elelem/version.rb
118
121
  - mise.toml
119
122
  - sig/elelem.rbs